Skip to content

Commit 8f201e8

Browse files
chore: bump js-yaml from 4.3.2 to 5.5.0 (#1898)
* chore: bump js-yaml from 4.3.2 to 5.5.0 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.2 to 5.5.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.3.2...5.5.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: preserve R Markdown YAML behavior with js-yaml 5 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kun Ren <renkun@outlook.com>
1 parent dd5bcce commit 8f201e8

7 files changed

Lines changed: 167 additions & 25 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2619,7 +2619,6 @@
26192619
},
26202620
"devDependencies": {
26212621
"@types/ejs": "^3.1.5",
2622-
"@types/js-yaml": "^4.0.9",
26232622
"@types/mocha": "^8.2.3",
26242623
"@types/node": "^22.20.5",
26252624
"@types/sinon": "^10.0.20",
@@ -2645,7 +2644,7 @@
26452644
"cheerio": "1.2.0",
26462645
"ejs": "^7.0.2",
26472646
"highlight.js": "^11.12.0",
2648-
"js-yaml": "^4.3.2",
2647+
"js-yaml": "^5.5.0",
26492648
"vscode-languageclient": "^10.1.2",
26502649
"winreg": "^1.2.5"
26512650
},

‎pnpm-lock.yaml‎

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

‎src/rTerminal.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { config, delay, getRterm, getCurrentWorkspaceFolder, getRPathConfigEntry
3030
import { resolveBackend, jgdEnabled, CommonPlotManager } from './plotViewer';
3131
import { tryInteractiveExecution } from './interactive/executionTarget';
3232
import * as fs from 'fs';
33-
import * as yaml from 'js-yaml';
33+
import { loadRMarkdownYaml } from './rmarkdown/yaml';
3434

3535
export let rTerm: vscode.Terminal | undefined = undefined;
3636
let rTermResource: vscode.Uri | undefined;
@@ -96,12 +96,6 @@ const terminalSends = new WeakMap<vscode.Terminal, Promise<void>>();
9696
let lastParamsRmdPath: string | undefined;
9797
let lastParamsRmdVersion: number | undefined;
9898

99-
const rExprType = new yaml.Type('!r', {
100-
kind: 'scalar',
101-
construct: (data: string) => ({ __rExpr: data }),
102-
});
103-
const RMARKDOWN_SCHEMA = yaml.DEFAULT_SCHEMA.extend([rExprType]);
104-
10599
function valueToR(val: unknown): string {
106100
if (val === null || val === undefined) {
107101
return 'NULL';
@@ -118,6 +112,12 @@ function valueToR(val: unknown): string {
118112
if (typeof val === 'object' && val !== null && '__rExpr' in (val as Record<string, unknown>)) {
119113
return (val as { __rExpr: string }).__rExpr;
120114
}
115+
if (val instanceof Set) {
116+
// js-yaml 5 represents !!set with Set instead of an object of nulls.
117+
return valueToR(
118+
Object.fromEntries(Array.from(val as Set<unknown>, (key) => [String(key), null])),
119+
);
120+
}
121121
if (Array.isArray(val)) {
122122
return `c(${val.map(valueToR).join(', ')})`;
123123
}
@@ -145,10 +145,7 @@ export function getRmdParamsCommand(document: vscode.TextDocument): string | und
145145
lastParamsRmdPath = filePath;
146146
lastParamsRmdVersion = document.version;
147147
try {
148-
const frontmatter = yaml.load(match[1], { schema: RMARKDOWN_SCHEMA }) as Record<
149-
string,
150-
unknown
151-
>;
148+
const frontmatter = loadRMarkdownYaml(match[1]) as Record<string, unknown>;
152149
const params = frontmatter?.['params'] as Record<string, unknown> | undefined;
153150
if (!params || typeof params !== 'object') {
154151
return undefined;

‎src/rmarkdown/knit.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as util from '../util';
22
import * as vscode from 'vscode';
33
import * as fs from 'node:fs';
44
import * as path from 'path';
5-
import * as yaml from 'js-yaml';
5+
import { loadRMarkdownYaml } from './yaml';
66

77
import { RMarkdownManager, KnitWorkingDirectory } from './manager';
88
import { runTextInTerm } from '../rTerminal';
@@ -125,7 +125,7 @@ export class RMarkdownKnitManager extends RMarkdownManager {
125125
let paramObj: IYamlFrontmatter = {};
126126
if (yamlText) {
127127
try {
128-
const parsed: unknown = yaml.load(yamlText);
128+
const parsed: unknown = loadRMarkdownYaml(yamlText);
129129
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
130130
paramObj = parsed as IYamlFrontmatter;
131131
}

‎src/rmarkdown/yaml.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as yaml from 'js-yaml';
2+
3+
const rExpressionTag = yaml.defineScalarTag('!r', {
4+
resolve: (source: string) => ({ __rExpr: source }),
5+
identify: () => false,
6+
});
7+
8+
const rMarkdownIntegerTag = {
9+
...yaml.intCoreTag,
10+
// v4 also accepted binary integers and signed octal/hexadecimal integers
11+
// without an explicit !!int tag. v5 restricts those to explicit tags.
12+
resolve: (source: string, _isExplicit: boolean, tagName: string) =>
13+
yaml.intCoreTag.resolve(source, true, tagName),
14+
};
15+
16+
// Keep the extra tags supported by js-yaml 4's DEFAULT_SCHEMA (including
17+
// timestamps and merge keys), with YAML 1.2 boolean and number resolution.
18+
const rMarkdownSchema = yaml.YAML11_SCHEMA.withTags(
19+
yaml.CORE_SCHEMA.tags,
20+
rMarkdownIntegerTag,
21+
rExpressionTag,
22+
);
23+
24+
export function loadRMarkdownYaml(source: string): unknown {
25+
// loadAll retains v4's undefined result for empty/comment-only headers.
26+
const documents = yaml.loadAll(source, { schema: rMarkdownSchema });
27+
if (documents.length > 1) {
28+
throw new yaml.YAMLException('expected a single YAML frontmatter document');
29+
}
30+
return documents[0];
31+
}

‎src/test/suite/rmarkdown.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,46 @@ suite('R Markdown execution', () => {
151151
);
152152
});
153153

154+
test('knit frontmatter preserves merged commands alongside !r parameters', async () => {
155+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-r-knit-yaml-'));
156+
try {
157+
const file = path.join(directory, 'test.Rmd');
158+
const knit = 'function(input, ...) rmarkdown::render(input, ...)';
159+
fs.writeFileSync(
160+
file,
161+
`---
162+
defaults: &defaults
163+
knit: ${knit}
164+
site: rmarkdown::render_site
165+
<<: *defaults
166+
date: !!timestamp 2026-11-07
167+
params:
168+
count: !r 1+1
169+
---\n`,
170+
);
171+
const manager = new RMarkdownKnitManager() as unknown as {
172+
getYamlFrontmatter(file: string): Record<string, unknown>;
173+
getKnitCommand(
174+
params: Record<string, unknown>,
175+
file: string,
176+
format?: string,
177+
): Promise<string>;
178+
};
179+
const frontmatter = manager.getYamlFrontmatter(file);
180+
assert.strictEqual(frontmatter['knit'], knit);
181+
assert.strictEqual(frontmatter['site'], 'rmarkdown::render_site');
182+
assert.deepStrictEqual(frontmatter['params'], { count: { __rExpr: '1+1' } });
183+
assert.deepStrictEqual(frontmatter['date'], new Date('2026-11-07T00:00:00Z'));
184+
const literal = util.ToRStringLiteral(file, '"');
185+
assert.strictEqual(
186+
await manager.getKnitCommand(frontmatter, literal, 'html_document'),
187+
`(${knit})(${literal}, output_format = "html_document")`,
188+
);
189+
} finally {
190+
fs.rmSync(directory, { recursive: true, force: true });
191+
}
192+
});
193+
154194
test('non-object YAML frontmatter cannot leave knitting permanently busy', async () => {
155195
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-r-knit-'));
156196
try {
@@ -178,7 +218,9 @@ suite('R Markdown execution', () => {
178218
) => Promise<void>;
179219
getYamlFrontmatter(file: string): unknown;
180220
};
221+
const parseError = sandbox.stub(console, 'error');
181222
assert.deepStrictEqual(manager.getYamlFrontmatter(file), {});
223+
sinon.assert.notCalled(parseError);
182224
const render = sandbox
183225
.stub(manager, 'renderDocument')
184226
.rejects(new Error('transient failure'));

‎src/test/suite/rmdParams.test.ts‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,87 @@ suite('Rmd Params Test Suite', () => {
6363
assert.strictEqual(cmd, 'params <- list(a = 1+1)');
6464
});
6565

66+
test('getRmdParamsCommand preserves merged parameter defaults and nested !r values', () => {
67+
const mockDoc = {
68+
languageId: 'rmd',
69+
getText: () => `---
70+
defaults: &defaults
71+
count: 2
72+
label: default
73+
params:
74+
<<: *defaults
75+
label: overridden
76+
expression:
77+
value: !r |
78+
sum(c(1, 2))
79+
values: [true, null, !r 1+1]
80+
---`,
81+
uri: vscode.Uri.file('/test_merged_params.Rmd'),
82+
version: 1,
83+
} as vscode.TextDocument;
84+
85+
assert.strictEqual(
86+
rTerminal.getRmdParamsCommand(mockDoc),
87+
'params <- list(count = 2, label = "overridden", expression = sum(c(1, 2))\n, values = c(TRUE, NULL, 1+1))',
88+
);
89+
});
90+
91+
test('getRmdParamsCommand keeps YAML 1.2 scalars with legacy metadata tags', () => {
92+
const mockDoc = {
93+
languageId: 'rmd',
94+
getText: () => `---
95+
date: !!timestamp 2026-11-07
96+
params:
97+
enabled: true
98+
label: yes
99+
mode: on
100+
count: 012
101+
binary: 0b101
102+
hex: -0xF
103+
nothing: null
104+
---`,
105+
uri: vscode.Uri.file('/test_scalar_params.Rmd'),
106+
version: 1,
107+
} as vscode.TextDocument;
108+
109+
assert.strictEqual(
110+
rTerminal.getRmdParamsCommand(mockDoc),
111+
'params <- list(enabled = TRUE, label = "yes", mode = "on", count = 12, binary = 5, hex = -15, nothing = NULL)',
112+
);
113+
});
114+
115+
test('getRmdParamsCommand preserves set parameters as named null values', () => {
116+
const mockDoc = {
117+
languageId: 'rmd',
118+
getText: () => '---\nparams:\n choices: !!set {one: null, two: null}\n---',
119+
uri: vscode.Uri.file('/test_set_params.Rmd'),
120+
version: 1,
121+
} as vscode.TextDocument;
122+
123+
assert.strictEqual(
124+
rTerminal.getRmdParamsCommand(mockDoc),
125+
'params <- list(choices = list(one = NULL, two = NULL))',
126+
);
127+
});
128+
129+
test('getRmdParamsCommand rejects malformed YAML and non-scalar !r tags', () => {
130+
for (const [index, value] of [
131+
'[1,',
132+
'!unknown value',
133+
'!r [1, 2]',
134+
'!r {a: 1}',
135+
].entries()) {
136+
const mockDoc = {
137+
languageId: 'rmd',
138+
getText: () => `---\nparams:\n a: ${value}\n---`,
139+
uri: vscode.Uri.file(`/test_invalid_params_${index}.Rmd`),
140+
version: 1,
141+
} as vscode.TextDocument;
142+
143+
assert.strictEqual(rTerminal.getRmdParamsCommand(mockDoc), undefined);
144+
}
145+
});
146+
66147
test('getRmdParamsCommand respects cache invalidation by version', () => {
67148
const mockDoc = {
68149
languageId: 'rmd',

0 commit comments

Comments
 (0)