Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/check/check.ts
3562 views
1
/*
2
* check.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import { info } from "../../deno_ral/log.ts";
8
9
import { render } from "../render/render-shared.ts";
10
import { renderServices } from "../render/render-services.ts";
11
12
import { JupyterCapabilities } from "../../core/jupyter/types.ts";
13
import { jupyterCapabilities } from "../../core/jupyter/capabilities.ts";
14
import {
15
jupyterCapabilitiesJson,
16
jupyterCapabilitiesMessage,
17
jupyterInstallationMessage,
18
jupyterUnactivatedEnvMessage,
19
pythonInstallationMessage,
20
} from "../../core/jupyter/jupyter-shared.ts";
21
import { completeMessage, withSpinner } from "../../core/console.ts";
22
import {
23
checkRBinary,
24
KnitrCapabilities,
25
knitrCapabilities,
26
knitrCapabilitiesMessage,
27
knitrInstallationMessage,
28
rInstallationMessage,
29
} from "../../core/knitr.ts";
30
import { quartoConfig } from "../../core/quarto.ts";
31
import {
32
cacheCodePage,
33
clearCodePageCache,
34
readCodePage,
35
} from "../../core/windows.ts";
36
import { RenderServiceWithLifetime } from "../render/types.ts";
37
import { jupyterKernelspecForLanguage } from "../../core/jupyter/kernels.ts";
38
import { execProcess } from "../../core/process.ts";
39
import { pandocBinaryPath } from "../../core/resources.ts";
40
import { lines } from "../../core/text.ts";
41
import { satisfies } from "semver/mod.ts";
42
import { dartCommand } from "../../core/dart-sass.ts";
43
import { allTools } from "../../tools/tools.ts";
44
import { texLiveContext, tlVersion } from "../render/latexmk/texlive.ts";
45
import { which } from "../../core/path.ts";
46
import { dirname } from "../../deno_ral/path.ts";
47
import { notebookContext } from "../../render/notebook/notebook-context.ts";
48
import { typstBinaryPath } from "../../core/typst.ts";
49
import { quartoCacheDir } from "../../core/appdirs.ts";
50
import { isWindows } from "../../deno_ral/platform.ts";
51
import { makeStringEnumTypeEnforcer } from "../../typing/dynamic.ts";
52
import { findChrome } from "../../core/puppeteer.ts";
53
54
export const kTargets = [
55
"install",
56
"info",
57
"jupyter",
58
"knitr",
59
"versions",
60
"all",
61
] as const;
62
export type Target = typeof kTargets[number];
63
export const enforceTargetType = makeStringEnumTypeEnforcer(...kTargets);
64
65
const kIndent = " ";
66
67
type CheckJsonResult = Record<string, unknown>;
68
69
type CheckConfiguration = {
70
strict: boolean;
71
target: Target;
72
output: string | undefined;
73
services: RenderServiceWithLifetime;
74
jsonResult: CheckJsonResult | undefined;
75
};
76
77
function checkCompleteMessage(conf: CheckConfiguration, message: string) {
78
if (!conf.jsonResult) {
79
completeMessage(message);
80
}
81
}
82
83
function checkInfoMsg(conf: CheckConfiguration, message: string) {
84
if (!conf.jsonResult) {
85
info(message);
86
}
87
}
88
89
export async function check(
90
target: Target,
91
strict?: boolean,
92
output?: string,
93
): Promise<void> {
94
const services = renderServices(notebookContext());
95
const conf: CheckConfiguration = {
96
strict: !!strict,
97
target: target,
98
output,
99
services,
100
jsonResult: undefined,
101
};
102
if (conf.output) {
103
conf.jsonResult = {
104
strict,
105
};
106
}
107
try {
108
if (conf.jsonResult) {
109
conf.jsonResult.version = quartoConfig.version();
110
}
111
checkInfoMsg(conf, `Quarto ${quartoConfig.version()}`);
112
113
for (
114
const [name, checker] of [
115
["info", checkInfo],
116
["versions", checkVersions],
117
["install", checkInstall],
118
["jupyter", checkJupyterInstallation],
119
["knitr", checkKnitrInstallation],
120
] as const
121
) {
122
if (target === name || target === "all") {
123
await checker(conf);
124
}
125
}
126
127
if (conf.jsonResult && conf.output) {
128
await Deno.writeTextFile(
129
conf.output,
130
JSON.stringify(conf.jsonResult, null, 2),
131
);
132
}
133
} finally {
134
services.cleanup();
135
}
136
}
137
138
// Currently this doesn't check anything
139
// but it's a placeholder for future checks
140
// and the message is useful for troubleshooting
141
async function checkInfo(conf: CheckConfiguration) {
142
const cacheDir = quartoCacheDir();
143
if (conf.jsonResult) {
144
conf.jsonResult!.info = { cacheDir };
145
}
146
checkCompleteMessage(conf, "Checking environment information...");
147
checkInfoMsg(conf, kIndent + "Quarto cache location: " + cacheDir);
148
}
149
150
async function checkVersions(conf: CheckConfiguration) {
151
const {
152
strict,
153
} = conf;
154
const checkVersion = (
155
version: string | undefined,
156
constraint: string,
157
name: string,
158
) => {
159
if (typeof version !== "string") {
160
throw new Error(`Unable to determine ${name} version`);
161
}
162
const good = satisfies(version, constraint);
163
if (conf.jsonResult) {
164
if (conf.jsonResult.dependencies === undefined) {
165
conf.jsonResult.dependencies = {};
166
}
167
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
168
version,
169
constraint,
170
satisfies: good,
171
};
172
}
173
if (!good) {
174
checkInfoMsg(
175
conf,
176
` NOTE: ${name} version ${version} is too old. Please upgrade to ${
177
constraint.slice(2)
178
} or later.`,
179
);
180
} else {
181
checkInfoMsg(conf, ` ${name} version ${version}: OK`);
182
}
183
};
184
185
const strictCheckVersion = (
186
version: string,
187
constraint: string,
188
name: string,
189
) => {
190
const good = version === constraint;
191
if (conf.jsonResult) {
192
if (conf.jsonResult.dependencies === undefined) {
193
conf.jsonResult.dependencies = {};
194
}
195
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
196
version,
197
constraint,
198
satisfies: good,
199
};
200
}
201
if (!good) {
202
checkInfoMsg(
203
conf,
204
` NOTE: ${name} version ${version} does not strictly match ${constraint} and strict checking is enabled. Please use ${constraint}.`,
205
);
206
} else {
207
checkInfoMsg(conf, ` ${name} version ${version}: OK`);
208
}
209
};
210
211
checkCompleteMessage(
212
conf,
213
"Checking versions of quarto binary dependencies...",
214
);
215
216
let pandocVersion = lines(
217
(await execProcess({
218
cmd: pandocBinaryPath(),
219
args: ["--version"],
220
stdout: "piped",
221
})).stdout!,
222
)[0]?.split(" ")[1];
223
const sassVersion = (await dartCommand(["--version"]))?.trim();
224
const denoVersion = Deno.version.deno;
225
const typstVersion = lines(
226
(await execProcess({
227
cmd: typstBinaryPath(),
228
args: ["--version"],
229
stdout: "piped",
230
})).stdout!,
231
)[0].split(" ")[1];
232
233
// We hack around pandocVersion to build a sem-verish string
234
// that satisfies the semver package
235
// if pandoc reports more than three version numbers, pick the first three
236
// if pandoc reports fewer than three version numbers, pad with zeros
237
if (pandocVersion) {
238
const versionParts = pandocVersion.split(".");
239
if (versionParts.length > 3) {
240
pandocVersion = versionParts.slice(0, 3).join(".");
241
} else if (versionParts.length < 3) {
242
pandocVersion = versionParts.concat(
243
Array(3 - versionParts.length).fill("0"),
244
).join(".");
245
}
246
}
247
248
// FIXME: all of these strict checks should be done by
249
// loading the configuration file directly, but that
250
// file is in an awkward format and it is not packaged
251
// with our installers
252
const checkData: [string | undefined, string, string][] = strict
253
? [
254
[pandocVersion, "3.6.3", "Pandoc"],
255
[sassVersion, "1.87.0", "Dart Sass"],
256
[denoVersion, "2.3.1", "Deno"],
257
[typstVersion, "0.13.0", "Typst"],
258
]
259
: [
260
[pandocVersion, ">=3.6.3", "Pandoc"],
261
[sassVersion, ">=1.87.0", "Dart Sass"],
262
[denoVersion, ">=2.3.1", "Deno"],
263
[typstVersion, ">=0.13.0", "Typst"],
264
];
265
const fun = strict ? strictCheckVersion : checkVersion;
266
for (const [version, constraint, name] of checkData) {
267
if (version === undefined) {
268
if (conf.jsonResult) {
269
if (conf.jsonResult.dependencies === undefined) {
270
conf.jsonResult.dependencies = {};
271
}
272
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
273
version,
274
constraint,
275
found: false,
276
};
277
}
278
checkInfoMsg(conf, ` ${name} version: (not detected)`);
279
} else {
280
fun(version, constraint, name);
281
}
282
}
283
284
checkCompleteMessage(
285
conf,
286
"Checking versions of quarto dependencies......OK",
287
);
288
}
289
290
async function checkInstall(conf: CheckConfiguration) {
291
const {
292
services,
293
} = conf;
294
checkCompleteMessage(conf, "Checking Quarto installation......OK");
295
checkInfoMsg(conf, `${kIndent}Version: ${quartoConfig.version()}`);
296
if (quartoConfig.version() === "99.9.9") {
297
// if they're running a dev version, we assume git is installed
298
// and QUARTO_ROOT is set to the root of the quarto-cli repo
299
// print the output of git rev-parse HEAD
300
const quartoRoot = Deno.env.get("QUARTO_ROOT");
301
if (quartoRoot) {
302
const gitHead = await execProcess({
303
cmd: "git",
304
args: ["-C", quartoRoot, "rev-parse", "HEAD"],
305
stdout: "piped",
306
stderr: "piped", // to not show error if not in a git repo
307
});
308
if (gitHead.success && gitHead.stdout) {
309
checkInfoMsg(conf, `${kIndent}commit: ${gitHead.stdout.trim()}`);
310
if (conf.jsonResult) {
311
conf.jsonResult["quarto-dev-version"] = gitHead.stdout.trim();
312
}
313
}
314
}
315
}
316
checkInfoMsg(conf, `${kIndent}Path: ${quartoConfig.binPath()}`);
317
if (conf.jsonResult) {
318
conf.jsonResult["quarto-path"] = quartoConfig.binPath();
319
}
320
321
if (isWindows) {
322
const json: Record<string, unknown> = {};
323
if (conf.jsonResult) {
324
conf.jsonResult.windows = json;
325
}
326
try {
327
const codePage = readCodePage();
328
clearCodePageCache();
329
await cacheCodePage();
330
const codePage2 = readCodePage();
331
332
checkInfoMsg(conf, `${kIndent}CodePage: ${codePage2 || "unknown"}`);
333
json["code-page"] = codePage2 || "unknown";
334
if (codePage && codePage !== codePage2) {
335
checkInfoMsg(
336
conf,
337
`${kIndent}NOTE: Code page updated from ${codePage} to ${codePage2}. Previous rendering may have been affected.`,
338
);
339
json["code-page-updated-from"] = codePage;
340
}
341
// if non-standard code page, check for non-ascii characters in path
342
// deno-lint-ignore no-control-regex
343
const nonAscii = /[^\x00-\x7F]+/;
344
if (nonAscii.test(quartoConfig.binPath())) {
345
checkInfoMsg(
346
conf,
347
`${kIndent}ERROR: Non-ASCII characters in Quarto path causes rendering problems.`,
348
);
349
json["non-ascii-in-path"] = true;
350
}
351
} catch {
352
checkInfoMsg(conf, `${kIndent}CodePage: Unable to read code page`);
353
json["error"] = "Unable to read code page";
354
}
355
}
356
357
checkInfoMsg(conf, "");
358
const toolsMessage = "Checking tools....................";
359
const toolsOutput: string[] = [];
360
let tools: Awaited<ReturnType<typeof allTools>>;
361
const toolsJson: Record<string, unknown> = {};
362
if (conf.jsonResult) {
363
conf.jsonResult.tools = toolsJson;
364
}
365
const toolsCb = async () => {
366
tools = await allTools();
367
368
for (const tool of tools.installed) {
369
const version = await tool.installedVersion() || "(external install)";
370
toolsOutput.push(`${kIndent}${tool.name}: ${version}`);
371
toolsJson[tool.name] = {
372
version,
373
};
374
}
375
for (const tool of tools.notInstalled) {
376
toolsOutput.push(`${kIndent}${tool.name}: (not installed)`);
377
toolsJson[tool.name] = {
378
installed: false,
379
};
380
}
381
};
382
if (conf.jsonResult) {
383
await toolsCb();
384
} else {
385
await withSpinner({
386
message: toolsMessage,
387
doneMessage: toolsMessage + "OK",
388
}, toolsCb);
389
}
390
toolsOutput.forEach((out) => checkInfoMsg(conf, out));
391
checkInfoMsg(conf, "");
392
393
const latexMessage = "Checking LaTeX....................";
394
const latexOutput: string[] = [];
395
const latexJson: Record<string, unknown> = {};
396
if (conf.jsonResult) {
397
conf.jsonResult.latex = latexJson;
398
}
399
const latexCb = async () => {
400
const tlContext = await texLiveContext(true);
401
if (tlContext.hasTexLive) {
402
const version = await tlVersion(tlContext);
403
404
if (tlContext.usingGlobal) {
405
const tlMgrPath = await which("tlmgr");
406
407
latexOutput.push(`${kIndent}Using: Installation From Path`);
408
if (tlMgrPath) {
409
latexOutput.push(`${kIndent}Path: ${dirname(tlMgrPath)}`);
410
latexJson["path"] = dirname(tlMgrPath);
411
latexJson["source"] = "global";
412
}
413
} else {
414
latexOutput.push(`${kIndent}Using: TinyTex`);
415
if (tlContext.binDir) {
416
latexOutput.push(`${kIndent}Path: ${tlContext.binDir}`);
417
latexJson["path"] = tlContext.binDir;
418
latexJson["source"] = "tinytex";
419
}
420
}
421
latexOutput.push(`${kIndent}Version: ${version}`);
422
latexJson["version"] = version;
423
} else {
424
latexOutput.push(`${kIndent}Tex: (not detected)`);
425
latexJson["installed"] = false;
426
}
427
};
428
if (conf.jsonResult) {
429
await latexCb();
430
} else {
431
await withSpinner({
432
message: latexMessage,
433
doneMessage: latexMessage + "OK",
434
}, latexCb);
435
}
436
latexOutput.forEach((out) => checkInfoMsg(conf, out));
437
checkInfoMsg(conf, "");
438
439
const chromeHeadlessMessage = "Checking Chrome Headless....................";
440
const chromeHeadlessOutput: string[] = [];
441
const chromeJson: Record<string, unknown> = {};
442
if (conf.jsonResult) {
443
conf.jsonResult.chrome = chromeJson;
444
}
445
const chromeCb = async () => {
446
const chromeDetected = await findChrome();
447
const chromiumQuarto = tools.installed.find((tool) =>
448
tool.name === "chromium"
449
);
450
if (chromeDetected.path !== undefined) {
451
chromeHeadlessOutput.push(`${kIndent}Using: Chrome found on system`);
452
chromeHeadlessOutput.push(
453
`${kIndent}Path: ${chromeDetected.path}`,
454
);
455
if (chromeDetected.source) {
456
chromeHeadlessOutput.push(`${kIndent}Source: ${chromeDetected.source}`);
457
}
458
chromeJson["path"] = chromeDetected.path;
459
chromeJson["source"] = chromeDetected.source;
460
} else if (chromiumQuarto !== undefined) {
461
chromeJson["source"] = "quarto";
462
chromeHeadlessOutput.push(
463
`${kIndent}Using: Chromium installed by Quarto`,
464
);
465
if (chromiumQuarto?.binDir) {
466
chromeHeadlessOutput.push(
467
`${kIndent}Path: ${chromiumQuarto?.binDir}`,
468
);
469
chromeJson["path"] = chromiumQuarto?.binDir;
470
}
471
chromeHeadlessOutput.push(
472
`${kIndent}Version: ${chromiumQuarto.installedVersion}`,
473
);
474
chromeJson["version"] = chromiumQuarto.installedVersion;
475
} else {
476
chromeHeadlessOutput.push(`${kIndent}Chrome: (not detected)`);
477
chromeJson["installed"] = false;
478
}
479
};
480
if (conf.jsonResult) {
481
await chromeCb();
482
} else {
483
await withSpinner({
484
message: chromeHeadlessMessage,
485
doneMessage: chromeHeadlessMessage + "OK",
486
}, chromeCb);
487
}
488
chromeHeadlessOutput.forEach((out) => checkInfoMsg(conf, out));
489
checkInfoMsg(conf, "");
490
491
const kMessage = "Checking basic markdown render....";
492
const markdownRenderJson: Record<string, unknown> = {};
493
if (conf.jsonResult) {
494
conf.jsonResult.render = {
495
markdown: markdownRenderJson,
496
};
497
}
498
const markdownRenderCb = async () => {
499
const mdPath = services.temp.createFile({ suffix: "check.md" });
500
Deno.writeTextFileSync(
501
mdPath,
502
`
503
---
504
title: "Title"
505
---
506
507
## Header
508
`,
509
);
510
const result = await render(mdPath, {
511
services,
512
flags: { quiet: true },
513
});
514
if (result.error) {
515
if (!conf.jsonResult) {
516
throw result.error;
517
} else {
518
markdownRenderJson["error"] = result.error;
519
}
520
} else {
521
markdownRenderJson["ok"] = true;
522
}
523
};
524
525
if (conf.jsonResult) {
526
await markdownRenderCb();
527
} else {
528
await withSpinner({
529
message: kMessage,
530
doneMessage: kMessage + "OK\n",
531
}, markdownRenderCb);
532
}
533
}
534
535
async function checkJupyterInstallation(conf: CheckConfiguration) {
536
const kMessage = "Checking Python 3 installation....";
537
const jupyterJson: Record<string, unknown> = {};
538
if (conf.jsonResult) {
539
(conf.jsonResult.tools as Record<string, unknown>).jupyter = jupyterJson;
540
}
541
let caps: JupyterCapabilities | undefined;
542
if (conf.jsonResult) {
543
caps = await jupyterCapabilities();
544
} else {
545
await withSpinner({
546
message: kMessage,
547
doneMessage: false,
548
}, async () => {
549
caps = await jupyterCapabilities();
550
});
551
}
552
if (caps) {
553
checkCompleteMessage(conf, kMessage + "OK");
554
if (conf.jsonResult) {
555
jupyterJson["capabilities"] = await jupyterCapabilitiesJson(caps);
556
} else {
557
checkInfoMsg(conf, await jupyterCapabilitiesMessage(caps, kIndent));
558
}
559
checkInfoMsg(conf, "");
560
if (caps.jupyter_core) {
561
if (await jupyterKernelspecForLanguage("python")) {
562
const kJupyterMessage = "Checking Jupyter engine render....";
563
if (conf.jsonResult) {
564
await checkJupyterRender(conf);
565
} else {
566
await withSpinner({
567
message: kJupyterMessage,
568
doneMessage: kJupyterMessage + "OK\n",
569
}, async () => {
570
await checkJupyterRender(conf);
571
});
572
}
573
} else {
574
jupyterJson["kernels"] = [];
575
checkInfoMsg(
576
conf,
577
kIndent + "NOTE: No Jupyter kernel for Python found",
578
);
579
checkInfoMsg(conf, "");
580
}
581
} else {
582
const installMessage = jupyterInstallationMessage(caps, kIndent);
583
checkInfoMsg(conf, installMessage);
584
checkInfoMsg(conf, "");
585
jupyterJson["installed"] = false;
586
jupyterJson["how-to-install"] = installMessage;
587
const envMessage = jupyterUnactivatedEnvMessage(caps, kIndent);
588
if (envMessage) {
589
checkInfoMsg(conf, envMessage);
590
checkInfoMsg(conf, "");
591
jupyterJson["env"] = {
592
"warning": envMessage,
593
};
594
}
595
}
596
} else {
597
checkCompleteMessage(conf, kMessage + "(None)\n");
598
const msg = pythonInstallationMessage(kIndent);
599
jupyterJson["installed"] = false;
600
jupyterJson["how-to-install-python"] = msg;
601
checkInfoMsg(conf, msg);
602
checkInfoMsg(conf, "");
603
}
604
}
605
606
async function checkJupyterRender(conf: CheckConfiguration) {
607
const {
608
services,
609
} = conf;
610
const json: Record<string, unknown> = {};
611
if (conf.jsonResult) {
612
(conf.jsonResult.render as Record<string, unknown>).jupyter = json;
613
}
614
const qmdPath = services.temp.createFile({ suffix: "check.qmd" });
615
Deno.writeTextFileSync(
616
qmdPath,
617
`
618
---
619
title: "Title"
620
---
621
622
## Header
623
624
\`\`\`{python}
625
1 + 1
626
\`\`\`
627
`,
628
);
629
const result = await render(qmdPath, {
630
services,
631
flags: { quiet: true, executeDaemon: 0 },
632
});
633
if (result.error) {
634
if (!conf.jsonResult) {
635
throw result.error;
636
} else {
637
json["error"] = result.error;
638
}
639
} else {
640
json["ok"] = true;
641
}
642
}
643
644
async function checkKnitrInstallation(conf: CheckConfiguration) {
645
const kMessage = "Checking R installation...........";
646
let caps: KnitrCapabilities | undefined;
647
let rBin: string | undefined;
648
const json: Record<string, unknown> = {};
649
if (conf.jsonResult) {
650
(conf.jsonResult.tools as Record<string, unknown>).knitr = json;
651
}
652
const knitrCb = async () => {
653
rBin = await checkRBinary();
654
caps = await knitrCapabilities(rBin);
655
};
656
if (conf.jsonResult) {
657
await knitrCb();
658
} else {
659
await withSpinner({
660
message: kMessage,
661
doneMessage: false,
662
}, knitrCb);
663
}
664
if (rBin && caps) {
665
checkCompleteMessage(conf, kMessage + "OK");
666
checkInfoMsg(conf, knitrCapabilitiesMessage(caps, kIndent));
667
checkInfoMsg(conf, "");
668
if (caps.packages.rmarkdownVersOk && caps.packages.knitrVersOk) {
669
const kKnitrMessage = "Checking Knitr engine render......";
670
if (conf.jsonResult) {
671
await checkKnitrRender(conf);
672
} else {
673
await withSpinner({
674
message: kKnitrMessage,
675
doneMessage: kKnitrMessage + "OK\n",
676
}, async () => {
677
await checkKnitrRender(conf);
678
});
679
}
680
} else {
681
// show install message if not available
682
// or update message if not up to date
683
json["installed"] = false;
684
if (!caps.packages.knitr || !caps.packages.knitrVersOk) {
685
const msg = knitrInstallationMessage(
686
kIndent,
687
"knitr",
688
!!caps.packages.knitr && !caps.packages.knitrVersOk,
689
);
690
checkInfoMsg(conf, msg);
691
json["how-to-install-knitr"] = msg;
692
}
693
if (!caps.packages.rmarkdown || !caps.packages.rmarkdownVersOk) {
694
const msg = knitrInstallationMessage(
695
kIndent,
696
"rmarkdown",
697
!!caps.packages.rmarkdown && !caps.packages.rmarkdownVersOk,
698
);
699
checkInfoMsg(conf, msg);
700
json["how-to-install-rmarkdown"] = msg;
701
}
702
checkInfoMsg(conf, "");
703
}
704
} else if (rBin === undefined) {
705
checkCompleteMessage(conf, kMessage + "(None)\n");
706
const msg = rInstallationMessage(kIndent);
707
checkInfoMsg(conf, msg);
708
json["installed"] = false;
709
checkInfoMsg(conf, "");
710
} else if (caps === undefined) {
711
json["installed"] = false;
712
checkCompleteMessage(conf, kMessage + "(None)\n");
713
const msgs = [
714
`R succesfully found at ${rBin}.`,
715
"However, a problem was encountered when checking configurations of packages.",
716
"Please check your installation of R.",
717
];
718
msgs.forEach((msg) => {
719
checkInfoMsg(conf, msg);
720
});
721
json["error"] = msgs.join("\n");
722
checkInfoMsg(conf, "");
723
}
724
}
725
726
async function checkKnitrRender(conf: CheckConfiguration) {
727
const {
728
services,
729
} = conf;
730
const json: Record<string, unknown> = {};
731
if (conf.jsonResult) {
732
(conf.jsonResult.render as Record<string, unknown>).knitr = json;
733
}
734
const rmdPath = services.temp.createFile({ suffix: "check.rmd" });
735
Deno.writeTextFileSync(
736
rmdPath,
737
`
738
---
739
title: "Title"
740
---
741
742
## Header
743
744
\`\`\`{r}
745
1 + 1
746
\`\`\`
747
`,
748
);
749
const result = await render(rmdPath, {
750
services,
751
flags: { quiet: true },
752
});
753
if (result.error) {
754
if (!conf.jsonResult) {
755
throw result.error;
756
} else {
757
json["error"] = result.error;
758
}
759
} else {
760
json["ok"] = true;
761
}
762
}
763
764