Skip to content

Commit 628d65d

Browse files
authored
Use yaml package instead of @yarnpkg/parsers to manipulate yaml (#36)
* Use yaml package to parse/dump yaml content * Add comments preservation tests * Remove @yarnpkg/parsers usage * Build plugin
1 parent beb93b3 commit 628d65d

File tree

8 files changed

+284
-57
lines changed

8 files changed

+284
-57
lines changed

bundles/@yarnpkg/plugin-catalogs.js

Lines changed: 143 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@
66
"@yarnpkg/cli": "^4.8.0",
77
"@yarnpkg/core": "^4.3.0",
88
"@yarnpkg/fslib": "^3.1.1",
9-
"@yarnpkg/parsers": "^3.0.2",
109
"@yarnpkg/plugin-essentials": "^4.3.2",
1110
"@yarnpkg/plugin-git": "^3.1.1",
1211
"@yarnpkg/plugin-pack": "^4.0.1",
1312
"chalk": "^5.4.1",
1413
"clipanion": "^4.0.0-rc.2",
15-
"picomatch": "^4.0.2"
14+
"picomatch": "^4.0.2",
15+
"yaml": "^2.8.1"
1616
},
1717
"devDependencies": {
18-
"@types/js-yaml": "^4.0.9",
1918
"@types/node": "^22.0.0",
2019
"@types/picomatch": "^4",
2120
"@types/tmp": "^0.2.6",
2221
"@yarnpkg/builder": "^4.2.0",
23-
"js-yaml": "^4.1.0",
2422
"rimraf": "5.0.0",
2523
"tmp-promise": "^3.0.3",
2624
"typescript": "^5.5.2",

sources/__tests__/apply-command.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,98 @@ describe("catalogs apply command", () => {
328328
const yarnrc = await workspace.readYarnrc();
329329
expect(yarnrc.catalogs).toBeUndefined();
330330
});
331+
332+
it("should preserve comments and formatting when updating catalogs", async () => {
333+
workspace = await createTestWorkspace();
334+
335+
const existingYarnrc = await workspace.readYarnrcRaw();
336+
await workspace.writeYarnrcRaw(`${existingYarnrc}
337+
# Yarn configuration
338+
nodeLinker: node-modules # inline comment
339+
340+
# Registry configuration
341+
npmRegistryServer: https://registry.npmjs.org
342+
343+
# Catalog configuration
344+
catalogs:
345+
stable:
346+
react: npm:17.0.0
347+
`);
348+
349+
await workspace.writeCatalogsYml({
350+
list: {
351+
stable: {
352+
react: "npm:18.0.0",
353+
},
354+
},
355+
});
356+
357+
await workspace.yarn.catalogs.apply();
358+
359+
const rawContent = await workspace.readYarnrcRaw();
360+
expect(rawContent).toContain("# Yarn configuration");
361+
expect(rawContent).toContain("# inline comment");
362+
expect(rawContent).toContain("# Registry configuration");
363+
expect(rawContent).toContain("# Catalog configuration");
364+
});
365+
366+
it("should preserve comments when updating both root and named catalogs", async () => {
367+
workspace = await createTestWorkspace();
368+
369+
const existingYarnrc = await workspace.readYarnrcRaw();
370+
await workspace.writeYarnrcRaw(`${existingYarnrc}
371+
# Root catalog
372+
catalog:
373+
lodash: npm:4.0.0
374+
375+
# Named catalogs
376+
catalogs:
377+
stable:
378+
react: npm:17.0.0
379+
`);
380+
381+
await workspace.writeCatalogsYml({
382+
list: {
383+
root: {
384+
lodash: "npm:4.17.21",
385+
},
386+
stable: {
387+
react: "npm:18.0.0",
388+
},
389+
},
390+
});
391+
392+
await workspace.yarn.catalogs.apply();
393+
394+
const rawContent = await workspace.readYarnrcRaw();
395+
expect(rawContent).toContain("# Root catalog");
396+
expect(rawContent).toContain("# Named catalogs");
397+
});
398+
399+
it("should preserve comments when removing catalogs", async () => {
400+
workspace = await createTestWorkspace();
401+
402+
const existingYarnrc = await workspace.readYarnrcRaw();
403+
await workspace.writeYarnrcRaw(`${existingYarnrc}
404+
# Config comment
405+
nodeLinker: node-modules
406+
407+
catalogs:
408+
stable:
409+
react: npm:18.0.0
410+
411+
# End comment
412+
`);
413+
414+
await workspace.writeCatalogsYml({
415+
list: {},
416+
});
417+
418+
await workspace.yarn.catalogs.apply();
419+
420+
const rawContent = await workspace.readYarnrcRaw();
421+
expect(rawContent).toContain("# Config comment");
422+
expect(rawContent).toContain("# End comment");
423+
expect(rawContent).not.toContain("catalogs:");
424+
});
331425
});

sources/__tests__/utils.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { execFile } from "node:child_process";
22
import { promisify } from "node:util";
33
import { type PortablePath, npath, ppath, xfs } from "@yarnpkg/fslib";
4-
import { dump as yamlDump, load as yamlLoad } from "js-yaml";
4+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
55
import { dir as tmpDir } from "tmp-promise";
66

77
const execFileAsync = promisify(execFile);
@@ -12,7 +12,9 @@ export interface TestWorkspace {
1212
writeJson: (path: string, content: unknown) => Promise<void>;
1313
readPackageJson: () => Promise<any>;
1414
readYarnrc: () => Promise<any>;
15+
readYarnrcRaw: () => Promise<string>;
1516
writeYarnrc: (content: unknown) => Promise<void>;
17+
writeYarnrcRaw: (content: string) => Promise<void>;
1618
writeCatalogsYml: (content: unknown) => Promise<void>;
1719
yarn: {
1820
(args: string[]): Promise<{ stdout: string; stderr: string }>;
@@ -76,7 +78,7 @@ export async function createTestWorkspace(): Promise<TestWorkspace> {
7678
.catch(() => "");
7779
await xfs.writeFilePromise(
7880
yarnrcPath,
79-
`${existingContent}\n${yamlDump(content)}`,
81+
`${existingContent}\n${yamlStringify(content)}`,
8082
);
8183
};
8284

@@ -85,7 +87,7 @@ export async function createTestWorkspace(): Promise<TestWorkspace> {
8587
portablePath,
8688
"catalogs.yml" as PortablePath,
8789
);
88-
await xfs.writeFilePromise(catalogsYmlPath, yamlDump(content));
90+
await xfs.writeFilePromise(catalogsYmlPath, yamlStringify(content));
8991
};
9092

9193
const readPackageJson = async () => {
@@ -97,7 +99,17 @@ export async function createTestWorkspace(): Promise<TestWorkspace> {
9799
const readYarnrc = async () => {
98100
const yarnrcPath = ppath.join(portablePath, ".yarnrc.yml" as PortablePath);
99101
const content = await xfs.readFilePromise(yarnrcPath, "utf8");
100-
return yamlLoad(content);
102+
return yamlParse(content);
103+
};
104+
105+
const readYarnrcRaw = async () => {
106+
const yarnrcPath = ppath.join(portablePath, ".yarnrc.yml" as PortablePath);
107+
return await xfs.readFilePromise(yarnrcPath, "utf8");
108+
};
109+
110+
const writeYarnrcRaw = async (content: string) => {
111+
const yarnrcPath = ppath.join(portablePath, ".yarnrc.yml" as PortablePath);
112+
await xfs.writeFilePromise(yarnrcPath, content);
101113
};
102114

103115
return {
@@ -106,7 +118,9 @@ export async function createTestWorkspace(): Promise<TestWorkspace> {
106118
writeJson,
107119
readPackageJson,
108120
readYarnrc,
121+
readYarnrcRaw,
109122
writeYarnrc: writeYaml,
123+
writeYarnrcRaw,
110124
writeCatalogsYml,
111125
yarn,
112126
};

sources/__tests__/validation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe("validation", () => {
156156

157157
const { stderr } = await workspace.yarn.add("[email protected]");
158158
expect(stderr).toContain("react");
159-
expect(stderr).toContain("beta, stable");
159+
expect(stderr).toContain("stable, beta");
160160
expect(stderr).toContain("react@catalog:");
161161
});
162162

sources/commands/apply.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { BaseCommand } from "@yarnpkg/cli";
22
import { Configuration, Project, StreamReport } from "@yarnpkg/core";
33
import { type Filename, type PortablePath, ppath, xfs } from "@yarnpkg/fslib";
4-
import { parseSyml, stringifySyml } from "@yarnpkg/parsers";
54
import chalk from "chalk";
65
import { Command, Option } from "clipanion";
6+
import { parseDocument, stringify } from "yaml";
77
import { configReader } from "../configuration";
88

99
export class ApplyCommand extends BaseCommand {
@@ -140,7 +140,8 @@ export async function readExistingYarnrc(
140140
}
141141

142142
const content = await xfs.readFilePromise(yarnrcPath, "utf8");
143-
return (parseSyml(content) as Record<string, unknown>) || {};
143+
const doc = parseDocument(content);
144+
return (doc.toJSON() as Record<string, unknown>) || {};
144145
}
145146

146147
/**
@@ -167,8 +168,8 @@ export function checkForChanges(
167168
newConfig.catalogs = undefined;
168169
}
169170

170-
const oldContent = stringifySyml(existingConfig);
171-
const newContent = stringifySyml(newConfig);
171+
const oldContent = stringify(existingConfig);
172+
const newContent = stringify(newConfig);
172173

173174
return oldContent !== newContent;
174175
}

sources/configuration/reader.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Project, Workspace } from "@yarnpkg/core";
22
import { structUtils } from "@yarnpkg/core";
33
import { type Filename, type PortablePath, ppath, xfs } from "@yarnpkg/fslib";
4-
import { parseSyml, stringifySyml } from "@yarnpkg/parsers";
54
import { isMatch } from "picomatch";
5+
import { parse, parseDocument } from "yaml";
66
import { ROOT_ALIAS_GROUP } from "../constants";
77
import { CatalogConfigurationError } from "../errors";
88
import type { CatalogsConfiguration } from "./types";
@@ -43,7 +43,7 @@ export class CatalogsConfigurationReader {
4343
}
4444

4545
const content = await xfs.readFilePromise(catalogsYmlPath, "utf8");
46-
const parsed: unknown = parseSyml(content);
46+
const parsed: unknown = parse(content);
4747

4848
if (!isValidCatalogsYml(parsed)) {
4949
throw new CatalogConfigurationError(
@@ -158,26 +158,26 @@ export class CatalogsConfigurationReader {
158158
".yarnrc.yml" as Filename as PortablePath,
159159
);
160160

161-
let existingConfig: Record<string, unknown> = {};
161+
let content = "";
162162
if (await xfs.existsPromise(yarnrcPath)) {
163-
const content = await xfs.readFilePromise(yarnrcPath, "utf8");
164-
existingConfig = (parseSyml(content) as Record<string, unknown>) || {};
163+
content = await xfs.readFilePromise(yarnrcPath, "utf8");
165164
}
166165

166+
const doc = parseDocument(content);
167+
167168
if (catalogs.root && Object.keys(catalogs.root).length > 0) {
168-
existingConfig.catalog = catalogs.root;
169+
doc.set("catalog", doc.createNode(catalogs.root));
169170
} else {
170-
existingConfig.catalog = undefined;
171+
doc.delete("catalog");
171172
}
172173

173174
if (Object.keys(catalogs.named).length > 0) {
174-
existingConfig.catalogs = catalogs.named;
175+
doc.set("catalogs", doc.createNode(catalogs.named));
175176
} else {
176-
existingConfig.catalogs = undefined;
177+
doc.delete("catalogs");
177178
}
178179

179-
const newContent = stringifySyml(existingConfig);
180-
await xfs.writeFilePromise(yarnrcPath, newContent);
180+
await xfs.writeFilePromise(yarnrcPath, doc.toString());
181181
}
182182

183183
/**

yarn.lock

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -610,13 +610,6 @@ __metadata:
610610
languageName: node
611611
linkType: hard
612612

613-
"@types/js-yaml@npm:^4.0.9":
614-
version: 4.0.9
615-
resolution: "@types/js-yaml@npm:4.0.9"
616-
checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211
617-
languageName: node
618-
linkType: hard
619-
620613
"@types/keyv@npm:^3.1.4":
621614
version: 3.1.4
622615
resolution: "@types/keyv@npm:3.1.4"
@@ -1964,13 +1957,6 @@ __metadata:
19641957
languageName: node
19651958
linkType: hard
19661959

1967-
"argparse@npm:^2.0.1":
1968-
version: 2.0.1
1969-
resolution: "argparse@npm:2.0.1"
1970-
checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e
1971-
languageName: node
1972-
linkType: hard
1973-
19741960
"assertion-error@npm:^1.1.0":
19751961
version: 1.1.0
19761962
resolution: "assertion-error@npm:1.1.0"
@@ -3021,17 +3007,6 @@ __metadata:
30213007
languageName: node
30223008
linkType: hard
30233009

3024-
"js-yaml@npm:^4.1.0":
3025-
version: 4.1.0
3026-
resolution: "js-yaml@npm:4.1.0"
3027-
dependencies:
3028-
argparse: "npm:^2.0.1"
3029-
bin:
3030-
js-yaml: bin/js-yaml.js
3031-
checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f
3032-
languageName: node
3033-
linkType: hard
3034-
30353010
"jsbn@npm:1.1.0":
30363011
version: 1.1.0
30373012
resolution: "jsbn@npm:1.1.0"
@@ -4570,30 +4545,37 @@ __metadata:
45704545
languageName: node
45714546
linkType: hard
45724547

4548+
"yaml@npm:^2.8.1":
4549+
version: 2.8.1
4550+
resolution: "yaml@npm:2.8.1"
4551+
bin:
4552+
yaml: bin.mjs
4553+
checksum: 10c0/7c587be00d9303d2ae1566e03bc5bc7fe978ba0d9bf39cc418c3139d37929dfcb93a230d9749f2cb578b6aa5d9ebebc322415e4b653cb83acd8bc0bc321707f3
4554+
languageName: node
4555+
linkType: hard
4556+
45734557
"yarn-plugin-catalogs@workspace:.":
45744558
version: 0.0.0-use.local
45754559
resolution: "yarn-plugin-catalogs@workspace:."
45764560
dependencies:
4577-
"@types/js-yaml": "npm:^4.0.9"
45784561
"@types/node": "npm:^22.0.0"
45794562
"@types/picomatch": "npm:^4"
45804563
"@types/tmp": "npm:^0.2.6"
45814564
"@yarnpkg/builder": "npm:^4.2.0"
45824565
"@yarnpkg/cli": "npm:^4.8.0"
45834566
"@yarnpkg/core": "npm:^4.3.0"
45844567
"@yarnpkg/fslib": "npm:^3.1.1"
4585-
"@yarnpkg/parsers": "npm:^3.0.2"
45864568
"@yarnpkg/plugin-essentials": "npm:^4.3.2"
45874569
"@yarnpkg/plugin-git": "npm:^3.1.1"
45884570
"@yarnpkg/plugin-pack": "npm:^4.0.1"
45894571
chalk: "npm:^5.4.1"
45904572
clipanion: "npm:^4.0.0-rc.2"
4591-
js-yaml: "npm:^4.1.0"
45924573
picomatch: "npm:^4.0.2"
45934574
rimraf: "npm:5.0.0"
45944575
tmp-promise: "npm:^3.0.3"
45954576
typescript: "npm:^5.5.2"
45964577
vitest: "npm:^1.3.1"
4578+
yaml: "npm:^2.8.1"
45974579
languageName: unknown
45984580
linkType: soft
45994581

0 commit comments

Comments
 (0)