Skip to content

Commit b80daa7

Browse files
authored
feat(json)!: add json.stringify: 'auto' and make that the default (#18303)
1 parent a514330 commit b80daa7

File tree

7 files changed

+194
-29
lines changed

7 files changed

+194
-29
lines changed

docs/config/shared-options.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,12 @@ Whether to support named imports from `.json` files.
345345

346346
## json.stringify
347347

348-
- **Type:** `boolean`
349-
- **Default:** `false`
348+
- **Type:** `boolean | 'auto'`
349+
- **Default:** `'auto'`
350350

351351
If set to `true`, imported JSON will be transformed into `export default JSON.parse("...")` which is significantly more performant than Object literals, especially when the JSON file is large.
352352

353-
Enabling this disables named imports.
353+
If set to `'auto'`, the data will be stringified only if [the data is bigger than 10kB](https://v8.dev/blog/cost-of-javascript-2019#json:~:text=A%20good%20rule%20of%20thumb%20is%20to%20apply%20this%20technique%20for%20objects%20of%2010%20kB%20or%20larger).
354354

355355
## esbuild
356356

packages/vite/src/node/__tests__/plugins/json.spec.ts

+112-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { expect, test } from 'vitest'
2-
import { extractJsonErrorPosition } from '../../plugins/json'
1+
import { describe, expect, test } from 'vitest'
2+
import {
3+
type JsonOptions,
4+
extractJsonErrorPosition,
5+
jsonPlugin,
6+
} from '../../plugins/json'
37

48
const getErrorMessage = (input: string) => {
59
try {
@@ -24,3 +28,109 @@ test('can extract json error position', () => {
2428
)
2529
}
2630
})
31+
32+
describe('transform', () => {
33+
const transform = (input: string, opts: JsonOptions, isBuild: boolean) => {
34+
const plugin = jsonPlugin(opts, isBuild)
35+
return (plugin.transform! as Function)(input, 'test.json').code
36+
}
37+
38+
test('namedExports: true, stringify: false', () => {
39+
const actual = transform(
40+
'{"a":1,\n"🫠": "",\n"const": false}',
41+
{ namedExports: true, stringify: false },
42+
false,
43+
)
44+
expect(actual).toMatchInlineSnapshot(`
45+
"export const a = 1;
46+
export default {
47+
a: a,
48+
"🫠": "",
49+
"const": false
50+
};
51+
"
52+
`)
53+
})
54+
55+
test('namedExports: false, stringify: false', () => {
56+
const actual = transform(
57+
'{"a":1,\n"🫠": "",\n"const": false}',
58+
{ namedExports: false, stringify: false },
59+
false,
60+
)
61+
expect(actual).toMatchInlineSnapshot(`
62+
"export default {
63+
a: 1,
64+
"🫠": "",
65+
"const": false
66+
};"
67+
`)
68+
})
69+
70+
test('namedExports: true, stringify: true', () => {
71+
const actual = transform(
72+
'{"a":1,\n"🫠": "",\n"const": false}',
73+
{ namedExports: true, stringify: true },
74+
false,
75+
)
76+
expect(actual).toMatchInlineSnapshot(`
77+
"export const a = 1;
78+
export default {
79+
a,
80+
"🫠": "",
81+
"const": false,
82+
};
83+
"
84+
`)
85+
})
86+
87+
test('namedExports: false, stringify: true', () => {
88+
const actualDev = transform(
89+
'{"a":1,\n"🫠": "",\n"const": false}',
90+
{ namedExports: false, stringify: true },
91+
false,
92+
)
93+
expect(actualDev).toMatchInlineSnapshot(
94+
`"export default JSON.parse("{\\"a\\":1,\\n\\"🫠\\": \\"\\",\\n\\"const\\": false}")"`,
95+
)
96+
97+
const actualBuild = transform(
98+
'{"a":1,\n"🫠": "",\n"const": false}',
99+
{ namedExports: false, stringify: true },
100+
true,
101+
)
102+
expect(actualBuild).toMatchInlineSnapshot(
103+
`"export default JSON.parse("{\\"a\\":1,\\"🫠\\":\\"\\",\\"const\\":false}")"`,
104+
)
105+
})
106+
107+
test("namedExports: true, stringify: 'auto'", () => {
108+
const actualSmall = transform(
109+
'{"a":1,\n"🫠": "",\n"const": false}',
110+
{ namedExports: true, stringify: 'auto' },
111+
false,
112+
)
113+
expect(actualSmall).toMatchInlineSnapshot(`
114+
"export const a = 1;
115+
export default {
116+
a,
117+
"🫠": "",
118+
"const": false,
119+
};
120+
"
121+
`)
122+
const actualLargeNonObject = transform(
123+
`{"a":1,\n"🫠": "${'vite'.repeat(3000)}",\n"const": false}`,
124+
{ namedExports: true, stringify: 'auto' },
125+
false,
126+
)
127+
expect(actualLargeNonObject).not.toContain('JSON.parse(')
128+
129+
const actualLarge = transform(
130+
`{"a":1,\n"🫠": {\n"foo": "${'vite'.repeat(3000)}"\n},\n"const": false}`,
131+
{ namedExports: true, stringify: 'auto' },
132+
false,
133+
)
134+
expect(actualLarge).toContain('JSON.parse(')
135+
})
136+
})

packages/vite/src/node/plugins/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export async function resolvePlugins(
7777
jsonPlugin(
7878
{
7979
namedExports: true,
80+
stringify: 'auto',
8081
...config.json,
8182
},
8283
isBuild,

packages/vite/src/node/plugins/json.ts

+59-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* https://github.com/rollup/plugins/blob/master/LICENSE
77
*/
88

9-
import { dataToEsm } from '@rollup/pluginutils'
9+
import { dataToEsm, makeLegalIdentifier } from '@rollup/pluginutils'
1010
import { SPECIAL_QUERY_RE } from '../constants'
1111
import type { Plugin } from '../plugin'
1212
import { stripBomTag } from '../utils'
@@ -19,10 +19,11 @@ export interface JsonOptions {
1919
namedExports?: boolean
2020
/**
2121
* Generate performant output as JSON.parse("stringified").
22-
* Enabling this will disable namedExports.
23-
* @default false
22+
*
23+
* When set to 'auto', the data will be stringified only if the data is bigger than 10kB.
24+
* @default 'auto'
2425
*/
25-
stringify?: boolean
26+
stringify?: boolean | 'auto'
2627
}
2728

2829
// Custom json filter for vite
@@ -47,24 +48,53 @@ export function jsonPlugin(
4748
json = stripBomTag(json)
4849

4950
try {
50-
if (options.stringify) {
51-
if (isBuild) {
51+
if (options.stringify !== false) {
52+
if (options.namedExports) {
53+
const parsed = JSON.parse(json)
54+
if (typeof parsed === 'object' && parsed != null) {
55+
const keys = Object.keys(parsed)
56+
57+
let code = ''
58+
let defaultObjectCode = '{\n'
59+
for (const key of keys) {
60+
if (key === makeLegalIdentifier(key)) {
61+
code += `export const ${key} = ${serializeValue(parsed[key])};\n`
62+
defaultObjectCode += ` ${key},\n`
63+
} else {
64+
defaultObjectCode += ` ${JSON.stringify(key)}: ${serializeValue(parsed[key])},\n`
65+
}
66+
}
67+
defaultObjectCode += '}'
68+
69+
code += `export default ${defaultObjectCode};\n`
70+
return {
71+
code,
72+
map: { mappings: '' },
73+
}
74+
}
75+
}
76+
77+
if (
78+
options.stringify === true ||
79+
// use 10kB as a threshold
80+
// https://v8.dev/blog/cost-of-javascript-2019#:~:text=A%20good%20rule%20of%20thumb%20is%20to%20apply%20this%20technique%20for%20objects%20of%2010%20kB%20or%20larger
81+
(options.stringify === 'auto' && json.length > 10 * 1000)
82+
) {
83+
// during build, parse then double-stringify to remove all
84+
// unnecessary whitespaces to reduce bundle size.
85+
if (isBuild) {
86+
json = JSON.stringify(JSON.parse(json))
87+
}
88+
5289
return {
53-
// during build, parse then double-stringify to remove all
54-
// unnecessary whitespaces to reduce bundle size.
55-
code: `export default JSON.parse(${JSON.stringify(
56-
JSON.stringify(JSON.parse(json)),
57-
)})`,
90+
code: `export default JSON.parse(${JSON.stringify(json)})`,
5891
map: { mappings: '' },
5992
}
60-
} else {
61-
return `export default JSON.parse(${JSON.stringify(json)})`
6293
}
6394
}
6495

65-
const parsed = JSON.parse(json)
6696
return {
67-
code: dataToEsm(parsed, {
97+
code: dataToEsm(JSON.parse(json), {
6898
preferConst: true,
6999
namedExports: options.namedExports,
70100
}),
@@ -81,6 +111,20 @@ export function jsonPlugin(
81111
}
82112
}
83113

114+
function serializeValue(value: unknown): string {
115+
const valueAsString = JSON.stringify(value)
116+
// use 10kB as a threshold
117+
// https://v8.dev/blog/cost-of-javascript-2019#:~:text=A%20good%20rule%20of%20thumb%20is%20to%20apply%20this%20technique%20for%20objects%20of%2010%20kB%20or%20larger
118+
if (
119+
typeof value === 'object' &&
120+
value != null &&
121+
valueAsString.length > 10 * 1000
122+
) {
123+
return `JSON.parse(${JSON.stringify(valueAsString)})`
124+
}
125+
return valueAsString
126+
}
127+
84128
export function extractJsonErrorPosition(
85129
errorMessage: string,
86130
inputLength: number,

packages/vite/src/node/server/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,13 @@ export async function _createServer(
542542
url: string,
543543
originalCode = code,
544544
) {
545-
return ssrTransform(code, inMap, url, originalCode, server.config)
545+
return ssrTransform(code, inMap, url, originalCode, {
546+
json: {
547+
stringify:
548+
config.json?.stringify === true &&
549+
config.json.namedExports !== true,
550+
},
551+
})
546552
},
547553
// environment.transformRequest and .warmupRequest don't take an options param for now,
548554
// so the logic and error handling needs to be duplicated here.

packages/vite/src/node/server/transformRequest.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -408,14 +408,15 @@ async function loadAndTransform(
408408
if (environment._closing && environment.config.dev.recoverable)
409409
throwClosedServerError()
410410

411+
const topLevelConfig = environment.getTopLevelConfig()
411412
const result = environment.config.dev.moduleRunnerTransform
412-
? await ssrTransform(
413-
code,
414-
normalizedMap,
415-
url,
416-
originalCode,
417-
environment.getTopLevelConfig(),
418-
)
413+
? await ssrTransform(code, normalizedMap, url, originalCode, {
414+
json: {
415+
stringify:
416+
topLevelConfig.json?.stringify === true &&
417+
topLevelConfig.json.namedExports !== true,
418+
},
419+
})
419420
: ({
420421
code,
421422
map: normalizedMap,

playground/json/__tests__/ssr/json-ssr.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ beforeEach(async () => {
1111
test('load json module', async () => {
1212
await untilUpdated(
1313
() => page.textContent('.fetch-json-module pre'),
14-
'export default JSON.parse("{\\n \\"hello\\": \\"hi\\"\\n}\\n")',
14+
'export const hello = "hi";\n' +
15+
'export default {\n' +
16+
' hello,\n' +
17+
'};\n',
1518
)
1619
})
1720

0 commit comments

Comments
 (0)