Skip to content

Commit ac2a918

Browse files
authored
refactor: ban JSON.stringify (#180)
1 parent cc4cb21 commit ac2a918

File tree

14 files changed

+67
-18
lines changed

14 files changed

+67
-18
lines changed

eslint.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import antfu from '@antfu/eslint-config'
2+
import pluginBan from 'eslint-plugin-ban'
23

34
export default antfu({
45
react: true,
56
formatters: true,
67
}, {
8+
plugins: { ban: pluginBan },
79
rules: {
810
'ts/consistent-type-definitions': 'off',
911
'react-refresh/only-export-components': 'off',
1012
'react/prefer-destructuring-assignment': 'off',
1113
'react/no-context-provider': 'off',
1214
'ts/method-signature-style': ['off'],
15+
'ban/ban': [
16+
'error',
17+
{
18+
name: ['JSON', 'stringify'],
19+
message: 'JSON.stringify can return undefined, use stringifyJSON instead',
20+
},
21+
],
1322
},
1423
}, {
1524
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test-d.ts', '**/*.test-d.tsx', 'apps/content/shared/**', 'playgrounds/**', 'packages/*/playground/**'],
@@ -18,6 +27,7 @@ export default antfu({
1827
'antfu/no-top-level-await': 'off',
1928
'react-hooks/rules-of-hooks': 'off',
2029
'no-alert': 'off',
30+
'ban/ban': 'off',
2131
},
2232
}, {
2333
files: ['apps/content/shared/**', 'apps/content/docs/**', 'apps/content/examples/**', 'playgrounds/**', 'packages/*/playground/**'],
@@ -26,6 +36,7 @@ export default antfu({
2636
'perfectionist/sort-imports': 'off',
2737
'import/first': 'off',
2838
'react-hooks/rules-of-hooks': 'off',
39+
'ban/ban': 'off',
2940
},
3041
}, {
3142
files: ['apps/content/examples/**'],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@vitest/coverage-v8": "^3.0.4",
3535
"@vue/test-utils": "^2.4.6",
3636
"eslint": "^9.15.0",
37+
"eslint-plugin-ban": "^2.0.0",
3738
"eslint-plugin-format": "^0.1.2",
3839
"eslint-plugin-react-hooks": "^5.0.0",
3940
"eslint-plugin-react-refresh": "^0.4.14",

packages/client/src/adapters/fetch/rpc-link.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Value } from '@orpc/shared'
22
import type { StandardBody } from '@orpc/standard-server'
33
import type { ClientContext, ClientLink, ClientOptionsOut } from '../../types'
44
import type { FetchWithContext } from './types'
5-
import { isAsyncIteratorObject, trim, value } from '@orpc/shared'
5+
import { isAsyncIteratorObject, stringifyJSON, trim, value } from '@orpc/shared'
66
import { toFetchBody, toStandardBody } from '@orpc/standard-server-fetch'
77
import { ORPCError } from '../../error'
88
import { createAutoRetryEventIterator, type EventIteratorReconnectOptions } from '../../event-iterator'
@@ -226,7 +226,7 @@ export class RPCLink<TClientContext extends ClientContext> implements ClientLink
226226
) {
227227
const getUrl = new URL(url)
228228

229-
getUrl.searchParams.append('data', JSON.stringify(serialized) ?? '') // stringify can return undefined
229+
getUrl.searchParams.append('data', stringifyJSON(serialized) ?? '')
230230

231231
if (getUrl.toString().length <= this.maxUrlLength) {
232232
return {

packages/client/src/rpc/serializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isAsyncIteratorObject } from '@orpc/shared'
1+
import { isAsyncIteratorObject, stringifyJSON } from '@orpc/shared'
22
import { ErrorEvent } from '@orpc/standard-server'
33
import { ORPCError, toORPCError } from '../error'
44
import { mapEventIterator } from '../event-iterator'
@@ -53,7 +53,7 @@ export class RPCSerializer {
5353

5454
const form = new FormData()
5555

56-
form.set('data', JSON.stringify({ json, meta, maps }))
56+
form.set('data', stringifyJSON({ json, meta, maps }))
5757

5858
blobs.forEach((blob, i) => {
5959
form.set(i.toString(), blob)

packages/server/src/adapters/standard/rpc-codec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class RPCCodec implements StandardCodec {
2020

2121
async decode(request: StandardRequest, _params: StandardParams | undefined, _procedure: AnyProcedure): Promise<unknown> {
2222
const serialized = request.method === 'GET'
23-
? parseEmptyableJSON(request.url.searchParams.getAll('data').at(-1) as any) // this prevent duplicate data params
23+
? parseEmptyableJSON(request.url.searchParams.getAll('data').at(-1)) // this prevent duplicate data params
2424
: await request.body() as any
2525

2626
return this.serializer.deserialize(serialized)

packages/shared/src/json.test-d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { stringifyJSON } from './json'
2+
3+
it('stringifyJSON', () => {
4+
expectTypeOf(stringifyJSON(undefined)).toEqualTypeOf<string | undefined>()
5+
expectTypeOf(stringifyJSON({})).toEqualTypeOf<string>()
6+
})

packages/shared/src/json.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { parseEmptyableJSON } from './json'
1+
import { parseEmptyableJSON, stringifyJSON } from './json'
22

33
it('parseEmptyableJSON', () => {
4+
expect(parseEmptyableJSON(undefined)).toBeUndefined()
5+
expect(parseEmptyableJSON(null)).toBeUndefined()
46
expect(parseEmptyableJSON('')).toBeUndefined()
57
expect(parseEmptyableJSON('{}')).toEqual({})
68
expect(parseEmptyableJSON('{"foo":"bar"}')).toEqual({ foo: 'bar' })
79
})
10+
11+
it('stringifyJSON', () => {
12+
expect(stringifyJSON(undefined)).toBeUndefined()
13+
expect(stringifyJSON({})).toEqual('{}')
14+
expect(stringifyJSON({ foo: 'bar' })).toEqual('{"foo":"bar"}')
15+
})

packages/shared/src/json.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
export function parseEmptyableJSON(text: string): unknown {
1+
export function parseEmptyableJSON(text: string | null | undefined): unknown {
22
if (!text) {
33
return undefined
44
}
55

66
return JSON.parse(text)
77
}
8+
9+
export function stringifyJSON<T>(value: T): undefined extends T ? undefined | string : string {
10+
// eslint-disable-next-line ban/ban
11+
return JSON.stringify(value)
12+
}

packages/standard-server-fetch/src/body.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { StandardBody } from '@orpc/standard-server'
2-
import { isAsyncIteratorObject, parseEmptyableJSON } from '@orpc/shared'
2+
import { isAsyncIteratorObject, parseEmptyableJSON, stringifyJSON } from '@orpc/shared'
33
import { contentDisposition, parseContentDisposition } from '@orpc/standard-server'
44
import { toEventIterator, toEventStream } from './event-source'
55

@@ -95,5 +95,5 @@ export function toFetchBody(
9595

9696
headers.set('content-type', 'application/json')
9797

98-
return JSON.stringify(body)
98+
return stringifyJSON(body)
9999
}

packages/standard-server-fetch/src/event-source.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isTypescriptObject, parseEmptyableJSON } from '@orpc/shared'
1+
import { isTypescriptObject, parseEmptyableJSON, stringifyJSON } from '@orpc/shared'
22
import {
33
encodeEventMessage,
44
ErrorEvent,
@@ -90,7 +90,7 @@ export function toEventStream(
9090
controller.enqueue(encodeEventMessage({
9191
...getEventMeta(value.value),
9292
event: value.done ? 'done' : 'message',
93-
data: JSON.stringify(value.value),
93+
data: stringifyJSON(value.value),
9494
}))
9595

9696
if (value.done) {
@@ -101,7 +101,7 @@ export function toEventStream(
101101
controller.enqueue(encodeEventMessage({
102102
...getEventMeta(err),
103103
event: 'error',
104-
data: err instanceof ErrorEvent ? JSON.stringify(err.data) : undefined,
104+
data: err instanceof ErrorEvent ? stringifyJSON(err.data) : undefined,
105105
}))
106106

107107
controller.close()

0 commit comments

Comments
 (0)