Skip to content

Commit d8e1c1f

Browse files
ocavuesxzz
andauthored
fix: skip package.json writting when content is deeply equal (#913)
Co-authored-by: Kevin Deng <sxzz@sxzz.moe>
1 parent 60592ef commit d8e1c1f

7 files changed

Lines changed: 155 additions & 53 deletions

File tree

packages/migrate/src/helpers/package-json.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
22
import { readFile, writeFile } from 'node:fs/promises'
33
import consola from 'consola'
44
import { createPatch } from 'diff'
5-
import { detectIndentation } from '../../../../src/utils/format.ts'
5+
import { detectIndentation } from '../../../../src/utils/json.ts'
66
import pkg from '../../package.json' with { type: 'json' }
77
import { outputDiff, renameKey } from '../utils.ts'
88

src/features/pkg/exports.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { readFileSync, writeFileSync } from 'node:fs'
21
import path from 'node:path'
32
import { RE_CSS, RE_DTS, RE_NODE_MODULES } from 'rolldown-plugin-dts/internal'
4-
import { detectIndentation } from '../../utils/format.ts'
53
import { stripExtname } from '../../utils/fs.ts'
64
import { matchPattern, slash, typeAssert } from '../../utils/general.ts'
5+
import { writeJsonFile } from '../../utils/json.ts'
76
import type { NormalizedFormat, ResolvedConfig } from '../../config/types.ts'
87
import type {
98
ChunksByFormat,
@@ -155,15 +154,7 @@ export async function writeExports(
155154
}
156155
}
157156

158-
const original = readFileSync(pkg.packageJsonPath, 'utf8')
159-
let contents = JSON.stringify(updatedPkg, null, detectIndentation(original))
160-
if (original.includes('\r\n')) {
161-
contents = contents.replaceAll('\n', '\r\n')
162-
}
163-
if (original.endsWith('\n')) contents += '\n'
164-
if (contents !== original) {
165-
writeFileSync(pkg.packageJsonPath, contents, 'utf8')
166-
}
157+
writeJsonFile(pkg.packageJsonPath, updatedPkg)
167158
}
168159

169160
type SubExport = Partial<Record<'cjs' | 'es' | 'src', string>>

src/run.ts

100644100755
File mode changed.

src/utils/format.test.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/utils/format.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,3 @@ export function formatBytes(bytes: number): string | undefined {
55
}
66
return `${(bytes / 1000).toFixed(2)} kB`
77
}
8-
9-
export function detectIndentation(jsonText: string): string | number {
10-
const lines = jsonText.split(/\r?\n/)
11-
12-
for (const line of lines) {
13-
const match = line.match(/^(\s+)\S/)
14-
if (!match) continue
15-
16-
if (match[1].includes('\t')) {
17-
return '\t'
18-
}
19-
return match[1].length
20-
}
21-
22-
return 2
23-
}

src/utils/json.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { readFileSync } from 'node:fs'
2+
import path from 'node:path'
3+
import { describe, expect, test } from 'vitest'
4+
import { writeFixtures } from '../../tests/utils.ts'
5+
import { detectIndentation, writeJsonFile } from './json.ts'
6+
7+
describe('writeJsonFile', () => {
8+
test('creates a new file when it does not exist', async (context) => {
9+
const { testDir } = await writeFixtures(context, { 'placeholder.txt': '' })
10+
const filePath = path.join(testDir, 'new.json')
11+
writeJsonFile(filePath, { foo: 'bar' })
12+
expect(readFileSync(filePath, 'utf8')).toBe('{\n "foo": "bar"\n}')
13+
})
14+
15+
test('does not rewrite when keys are reordered but content is deeply equal', async (context) => {
16+
const original = '{"b":1,\n"a":2}'
17+
const { testDir } = await writeFixtures(context, { 'pkg.json': original })
18+
const filePath = path.join(testDir, 'pkg.json')
19+
writeJsonFile(filePath, { a: 2, b: 1 })
20+
expect(readFileSync(filePath, 'utf8')).toBe(original)
21+
})
22+
23+
test('does not rewrite when content is identical', async (context) => {
24+
const original = '{\t"foo":"bar"\n }'
25+
const { testDir } = await writeFixtures(context, { 'pkg.json': original })
26+
const filePath = path.join(testDir, 'pkg.json')
27+
writeJsonFile(filePath, { foo: 'bar' })
28+
expect(readFileSync(filePath, 'utf8')).toBe(original)
29+
})
30+
31+
test('updates the file when content changes', async (context) => {
32+
const { testDir } = await writeFixtures(context, {
33+
'pkg.json': '{\n "foo": "bar"\n}',
34+
})
35+
const filePath = path.join(testDir, 'pkg.json')
36+
writeJsonFile(filePath, { foo: 'baz' })
37+
expect(readFileSync(filePath, 'utf8')).toBe('{\n "foo": "baz"\n}')
38+
})
39+
40+
test('preserves tab indentation', async (context) => {
41+
const { testDir } = await writeFixtures(context, {
42+
'pkg.json': '{\n\t"foo": "bar"\n}',
43+
})
44+
const filePath = path.join(testDir, 'pkg.json')
45+
writeJsonFile(filePath, { foo: 'baz' })
46+
expect(readFileSync(filePath, 'utf8')).toBe('{\n\t"foo": "baz"\n}')
47+
})
48+
49+
test('preserves 4-space indentation', async (context) => {
50+
const { testDir } = await writeFixtures(context, {
51+
'pkg.json': '{\n "foo": "bar"\n}',
52+
})
53+
const filePath = path.join(testDir, 'pkg.json')
54+
writeJsonFile(filePath, { foo: 'baz' })
55+
expect(readFileSync(filePath, 'utf8')).toBe('{\n "foo": "baz"\n}')
56+
})
57+
58+
test('preserves CRLF line endings', async (context) => {
59+
const { testDir } = await writeFixtures(context, {
60+
'pkg.json': '{\r\n "foo": "bar"\r\n}',
61+
})
62+
const filePath = path.join(testDir, 'pkg.json')
63+
writeJsonFile(filePath, { foo: 'baz' })
64+
expect(readFileSync(filePath, 'utf8')).toBe('{\r\n "foo": "baz"\r\n}')
65+
})
66+
67+
test('preserves trailing newline', async (context) => {
68+
const { testDir } = await writeFixtures(context, {
69+
'pkg.json': '{\n "foo": "bar"\n}\n',
70+
})
71+
const filePath = path.join(testDir, 'pkg.json')
72+
writeJsonFile(filePath, { foo: 'baz' })
73+
expect(readFileSync(filePath, 'utf8')).toBe('{\n "foo": "baz"\n}\n')
74+
})
75+
})
76+
77+
describe('detectIndent', () => {
78+
test('two spaces', ({ expect }) => {
79+
expect(detectIndentation(stringifyJson(2))).toBe(2)
80+
})
81+
test('four spaces', ({ expect }) => {
82+
expect(detectIndentation(stringifyJson(4))).toBe(4)
83+
})
84+
test('tab', ({ expect }) => {
85+
expect(detectIndentation(stringifyJson('\t'))).toBe('\t')
86+
})
87+
test('empty', ({ expect }) => {
88+
expect(detectIndentation('')).toBe(2)
89+
})
90+
test('empty line', ({ expect }) => {
91+
expect(detectIndentation('{\n\n "foo": 42 }')).toBe(2)
92+
})
93+
})
94+
95+
function stringifyJson(indentation: string | number): string {
96+
const contents = JSON.stringify({ foo: 42 }, null, indentation)
97+
return contents
98+
}

src/utils/json.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { readFileSync, writeFileSync } from 'node:fs'
2+
import { isDeepStrictEqual } from 'node:util'
3+
4+
export function writeJsonFile(filePath: string, content: unknown): void {
5+
let originalContent: unknown = undefined
6+
let originalIndent: string | number = 2
7+
let originalEOL: string = '\n'
8+
let originalHasTrailingNewline: boolean = false
9+
10+
try {
11+
const text = readFileSync(filePath, 'utf8')
12+
originalContent = JSON.parse(text)
13+
originalIndent = detectIndentation(text)
14+
if (text.includes('\r\n')) {
15+
originalEOL = '\r\n'
16+
}
17+
if (text.endsWith('\n')) {
18+
originalHasTrailingNewline = true
19+
}
20+
} catch {
21+
// File doesn't exist or isn't valid JSON, we'll overwrite it with our content
22+
}
23+
24+
if (originalContent && isDeepStrictEqual(originalContent, content)) {
25+
// The content is the same. We just return without updating the file format
26+
return
27+
}
28+
29+
let jsonString = JSON.stringify(content, null, originalIndent)
30+
if (originalEOL !== '\n') {
31+
jsonString = jsonString.replaceAll('\n', originalEOL)
32+
}
33+
if (originalHasTrailingNewline) {
34+
jsonString += originalEOL
35+
}
36+
37+
writeFileSync(filePath, jsonString, 'utf8')
38+
}
39+
40+
export function detectIndentation(jsonText: string): string | number {
41+
const lines = jsonText.split(/\r?\n/)
42+
43+
for (const line of lines) {
44+
const match = line.match(/^(\s+)\S/)
45+
if (!match) continue
46+
47+
if (match[1].includes('\t')) {
48+
return '\t'
49+
}
50+
return match[1].length
51+
}
52+
53+
return 2
54+
}

0 commit comments

Comments
 (0)