Skip to content

Commit 4fa7c52

Browse files
danielbayleyantfu
andauthored
fix: preserve comments and format in package.yaml (#225)
* fix: preserve comments and format in package.yaml * test: fixes to package.yaml tests * chore: update deps --------- Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 09908df commit 4fa7c52

File tree

3 files changed

+65
-26
lines changed

3 files changed

+65
-26
lines changed

src/io/packageYaml.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import type { Document as DocumentType } from 'yaml'
12
import type { CommonOptions, DepType, PackageMeta, RawDep } from '../types'
23
import * as fs from 'node:fs/promises'
34
import detectIndent from 'detect-indent'
45
import { resolve } from 'pathe'
5-
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
6+
import { Document, parseDocument as parseYaml, stringify as stringifyYaml } from 'yaml'
67
import { builtinAddons } from '../addons'
7-
import { dumpDependencies, getByPath, parseDependencies, parseDependency, setByPath } from './dependencies'
8+
import { dumpDependencies, getByPath, parseDependencies, parseDependency } from './dependencies'
89

910
const allDepsFields = [
1011
'dependencies',
@@ -21,20 +22,21 @@ function isDepFieldEnabled(key: DepType, options: CommonOptions): boolean {
2122
return key === 'peerDependencies' ? !!options.peer : options.depFields?.[key] !== false
2223
}
2324

24-
export async function readYAML(filepath: string): Promise<Record<string, unknown>> {
25+
export async function readYAML(filepath: string): Promise<DocumentType> {
2526
const content = await fs.readFile(filepath, 'utf-8')
2627
if (!content)
27-
return {}
28+
return new Document({})
2829

29-
const parsed = parseYaml(content)
30+
const document = parseYaml(content, { merge: true })
31+
const parsed = document.toJS()
3032

31-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
33+
if (document.errors.length || typeof parsed !== 'object' || Array.isArray(parsed))
3234
throw new TypeError(`Invalid package.yaml structure in ${filepath}`)
3335

34-
return parsed as Record<string, unknown>
36+
return document
3537
}
3638

37-
export async function writeYAML(filepath: string, data: Record<string, unknown>) {
39+
export async function writeYAML(filepath: string, data: DocumentType | Record<string, unknown>) {
3840
const { amount, type } = await fs.readFile(filepath, 'utf-8')
3941
.then(detectIndent)
4042
.catch(Object.create)
@@ -64,22 +66,23 @@ export async function loadPackageYAML(
6466
continue
6567

6668
if (key === 'packageManager') {
67-
if (raw.packageManager && typeof raw.packageManager === 'string') {
68-
const [name, version] = raw.packageManager.split('@')
69+
const packageManager = raw.get(key)
70+
if (typeof packageManager === 'string') {
71+
const [name, version] = packageManager.split('@')
6972
// `+` sign can be used to pin the hash of the package manager, we remove it to be semver compatible.
7073
deps.push(parseDependency(name, `^${version.split('+')[0]}`, 'packageManager', shouldUpdate))
7174
}
7275
}
7376
else {
74-
deps.push(...parseDependencies(raw, key, shouldUpdate))
77+
deps.push(...parseDependencies(raw.toJS(), key, shouldUpdate))
7578
}
7679
}
7780

7881
return [
7982
{
80-
name: typeof raw.name === 'string' ? raw.name : '',
81-
private: !!raw.private,
82-
version: typeof raw.version === 'string' ? raw.version : '',
83+
name: raw.get('name') as string ?? '',
84+
private: !!raw.get('private'),
85+
version: raw.get('version') as string ?? '',
8386
type: 'package.yaml',
8487
relative,
8588
filepath,
@@ -101,16 +104,18 @@ export async function writePackageYAML(
101104
continue
102105

103106
if (key === 'packageManager') {
104-
const value = Object.entries(dumpDependencies(pkg.resolved, 'packageManager'))[0]
107+
const [value] = Object.entries(dumpDependencies(pkg.resolved, 'packageManager'))
105108
if (value) {
106-
pkg.raw ||= {}
107-
pkg.raw.packageManager = `${value[0]}@${value[1].replace('^', '')}`
109+
pkg.raw ??= new Document({})
110+
pkg.raw.set('packageManager', `${value[0]}@${value[1].replace('^', '')}`)
108111
changed = true
109112
}
110113
}
111114
else {
112-
if (getByPath(pkg.raw, key)) {
113-
setByPath(pkg.raw, key, dumpDependencies(pkg.resolved, key))
115+
if (getByPath(pkg.raw?.toJS?.(), key)) {
116+
const values = Object.entries(dumpDependencies(pkg.resolved, key))
117+
values.forEach(([lastKey, value]) =>
118+
pkg.raw?.setIn([...key.split('.'), lastKey], value))
114119
changed = true
115120
}
116121
}

src/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Agent } from 'package-manager-detector'
22
import type { PnpmWorkspaceYaml } from 'pnpm-workspace-yaml'
3+
import type { Document } from 'yaml'
34
import type { MODE_CHOICES } from './constants'
45
import type { SortOption } from './utils/sort'
56

@@ -241,9 +242,9 @@ export interface PackageYamlMeta extends BasePackageMeta {
241242
*/
242243
type: 'package.yaml'
243244
/**
244-
* Raw package.yaml Object
245+
* Raw package.yaml Document
245246
*/
246-
raw: Record<string, unknown>
247+
raw: Document
247248
}
248249

249250
export type PackageMeta

test/packageYaml.test.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { Document as DocumentType } from 'yaml'
12
import type { CheckOptions, PackageYamlMeta } from '../src/types'
23
import process from 'node:process'
34
import { afterEach, beforeEach, describe, expect, it, vi, vitest } from 'vitest'
5+
import { Document } from 'yaml'
46
import { CheckPackages } from '../src'
57
import * as packageYaml from '../src/io/packageYaml'
68

@@ -111,7 +113,7 @@ describe('package.yaml functionality', () => {
111113
type: 'package.yaml',
112114
filepath: '/tmp/package.yaml',
113115
relative: 'package.yaml',
114-
raw: {
116+
raw: new Document({
115117
name: '@taze/package-yaml-example',
116118
version: '1.0.0',
117119
dependencies: {
@@ -122,7 +124,7 @@ describe('package.yaml functionality', () => {
122124
'@types/lodash': '^4.14.0',
123125
'typescript': '3.5',
124126
},
125-
},
127+
}),
126128
deps: [],
127129
resolved: [
128130
{
@@ -165,8 +167,8 @@ describe('package.yaml functionality', () => {
165167
version: "1.0.0"
166168
# This is a comment
167169
dependencies:
168-
lodash: "^4.13.19" # inline comment
169-
express: "4.12.x"
170+
lodash: ^4.13.19 # inline comment
171+
express: 4.12.x
170172
171173
devDependencies:
172174
typescript: "3.5"
@@ -177,12 +179,43 @@ devDependencies:
177179
// Mock readFile to return our YAML content
178180
vi.mocked(await import('node:fs/promises')).readFile = vi.fn().mockResolvedValue(yamlContent)
179181

180-
const raw = await packageYaml.readYAML(filepath)
182+
const doc: DocumentType = await packageYaml.readYAML(filepath)
183+
const raw = doc.toJS()
184+
181185
expect(raw.name).toBe('@taze/test')
182186
expect(raw.dependencies).toEqual({
183187
lodash: '^4.13.19',
184188
express: '4.12.x',
185189
})
190+
191+
const pkgYaml: PackageYamlMeta = {
192+
name: raw.name,
193+
version: raw.version,
194+
private: false,
195+
type: 'package.yaml',
196+
filepath: '/tmp/package.yaml',
197+
relative: 'package.yaml',
198+
raw: doc,
199+
deps: [],
200+
resolved: [
201+
{
202+
name: 'lodash',
203+
currentVersion: '^4.13.19',
204+
targetVersion: '^4.17.21',
205+
source: 'dependencies',
206+
update: true,
207+
diff: 'minor',
208+
pkgData: { tags: { latest: '4.17.21' }, versions: ['4.17.21'] },
209+
provenanceDowngraded: false,
210+
},
211+
],
212+
}
213+
214+
await packageYaml.writePackageYAML(pkgYaml, {})
215+
216+
expect(output).toContain('# This is a comment')
217+
expect(output).toContain('# inline comment')
218+
expect(output).toBe(yamlContent.replace('4.13.19', '4.17.21'))
186219
})
187220

188221
it('should detect package.yaml as higher priority than package.json', async () => {

0 commit comments

Comments
 (0)