Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/project/project-environment.ts
6458 views
1
/*
2
* project-environment.ts
3
*
4
* Copyright (C) 2021-2023 Posit Software, PBC
5
*/
6
7
import { extname } from "../deno_ral/path.ts";
8
import { existsSync } from "../deno_ral/fs.ts";
9
import { projectType } from "../project/types/project-types.ts";
10
import {
11
kManuscriptType,
12
ResolvedManuscriptConfig,
13
} from "../project/types/manuscript/manuscript-types.ts";
14
import { isPdfOutput } from "../config/format.ts";
15
import { ProjectContext } from "../project/types.ts";
16
import { kLocalDevelopment, quartoConfig } from "../core/quarto.ts";
17
import { ProjectEnvironment } from "./project-environment-types.ts";
18
import { gitHubContext } from "../core/github.ts";
19
import { SemVer } from "semver/mod.ts";
20
import { QuartoEditor, QuartoTool } from "./project-environment-types.ts";
21
import { withRenderServices } from "../command/render/render-services.ts";
22
import { NotebookContext } from "../render/notebook/notebook-types.ts";
23
24
const kDefaultContainerTitle = "Default Container";
25
26
export const makeProjectEnvironmentMemoizer = (
27
notebookContext: NotebookContext,
28
) => {
29
let cachedEnv: ProjectEnvironment | undefined = undefined;
30
return async (project: ProjectContext) => {
31
if (cachedEnv) {
32
return Promise.resolve(cachedEnv);
33
} else {
34
cachedEnv = await computeProjectEnvironment(notebookContext, project);
35
return cachedEnv;
36
}
37
};
38
};
39
40
export const computeProjectEnvironment = async (
41
notebookContext: NotebookContext,
42
context: ProjectContext,
43
) => {
44
// Get the quarto version
45
const version = quartoConfig.version();
46
const quarto = version === kLocalDevelopment
47
? "prerelease"
48
: new SemVer(version);
49
50
// Compute the GitHub Context
51
const github = await gitHubContext(context.dir);
52
53
const containerCtx: ProjectEnvironment = {
54
title: kDefaultContainerTitle,
55
engines: context.engines,
56
tools: [],
57
codeEnvironment: "vscode",
58
quarto,
59
environments: [],
60
openFiles: [],
61
envVars: {},
62
github,
63
};
64
65
// Figure out the editor
66
const editorContext = projectEditor(context);
67
containerCtx.codeEnvironment = editorContext.editor;
68
containerCtx.openFiles.push(...editorContext.openFiles);
69
70
// Determine the title
71
const title = context.config?.project.title;
72
if (title) {
73
containerCtx.title = title;
74
}
75
76
// Determine what tools (if any) we should also install
77
const tools = await projectTools(notebookContext, context);
78
containerCtx.tools.push(...tools);
79
80
// Determine environments
81
const envFiles = Object.keys(environmentCommands);
82
for (const envFile of envFiles) {
83
if (existsSync(envFile)) {
84
containerCtx.environments.push(envFile);
85
}
86
}
87
88
return containerCtx;
89
};
90
91
interface EnvironmentOptions {
92
restore?: string;
93
features?: Record<string, Record<string, unknown>>;
94
}
95
96
const environmentCommands: Record<string, EnvironmentOptions> = {
97
// TODO: this needs to happen in correct directory post setup
98
// options(repos = c(REPO_NAME = "https://packagemanager.posit.co/cran/__linux__/jammy/latest"))
99
"renv.lock": {
100
restore: `Rscript -e 'renv::restore();'`,
101
},
102
"requirements.txt": {
103
restore: `python3 -m pip install -r requirements.txt`,
104
},
105
"DESCRIPTION": {
106
restore: `Rscript -e 'devtools::install_local(getwd())'`,
107
},
108
"install.R": {
109
restore: `Rscript install.R`,
110
},
111
"environment.yml": {
112
restore: "conda env create -f environment.yml",
113
features: {
114
"ghcr.io/devcontainers/features/conda:1": {
115
addCondaForge: true,
116
},
117
},
118
},
119
"PipFile": {},
120
"PipFile.lock": {},
121
"setup.py": {},
122
"Project.toml": {},
123
"REQUIRE": {},
124
};
125
126
// Regex used to determine whether file contents will require the installation of Chromium
127
const kChromiumHint = /````*{mermaid}|{dot}/gm;
128
129
const projectTools = async (
130
notebookContext: NotebookContext,
131
context: ProjectContext,
132
) => {
133
// Determine what tools (if any) we should also install
134
let tinytex = false;
135
let chromium = false;
136
137
for (const input of context.files.input) {
138
if (!tinytex) {
139
// If we haven't yet found the need for tinytex,
140
// go ahead and look for PDF format. Once a single
141
// file needs, it we can stop looking
142
const formats = await withRenderServices(
143
notebookContext,
144
(services) => context.renderFormats(input, services, "all", context),
145
);
146
147
const hasPdf = Object.values(formats).some((format) => {
148
return isPdfOutput(format.pandoc);
149
});
150
tinytex = hasPdf;
151
}
152
153
// See if the file contains mermaid or graphviZ
154
if (!chromium) {
155
const contents = Deno.readTextFileSync(input);
156
if (contents.match(kChromiumHint)) {
157
chromium = true;
158
}
159
}
160
161
if (tinytex && chromium) {
162
break;
163
}
164
}
165
166
const tools: QuartoTool[] = [];
167
if (tinytex) {
168
tools.push("tinytex");
169
}
170
if (chromium) {
171
tools.push("chromium");
172
}
173
return tools;
174
};
175
176
const projectEditor = (context: ProjectContext) => {
177
const qmdCodeTool = context.engines.includes("knitr") ? "rstudio" : "vscode";
178
const ipynbCodeTool = "jupyterlab";
179
180
const openFiles: string[] = [];
181
let editor: QuartoEditor = qmdCodeTool;
182
183
// Determine the code environment
184
// Special case manuscripts - the root article will drive the code environment
185
if (projectType(context.config?.project.type).type === kManuscriptType) {
186
// Choose the code environment based upon the engine and article file type
187
const manuscriptConfig = context.config
188
?.[kManuscriptType] as ResolvedManuscriptConfig;
189
if (extname(manuscriptConfig.article) === ".qmd") {
190
editor = qmdCodeTool;
191
} else {
192
editor = ipynbCodeTool;
193
}
194
195
// Open the main article file
196
openFiles.push(manuscriptConfig.article);
197
} else {
198
// Count the ipynb vs qmds and use that as guideline
199
const exts: Record<string, number> = {};
200
const inputs = context.files.input;
201
for (const input of inputs) {
202
const ext = extname(input);
203
exts[ext] = (exts[ext] || 0) + 1;
204
}
205
206
const qmdCount = exts[".qmd"] || 0;
207
const ipynbCount = exts[".ipynb"] || 0;
208
if (qmdCount >= ipynbCount) {
209
editor = qmdCodeTool;
210
} else {
211
editor = ipynbCodeTool;
212
}
213
}
214
return {
215
editor,
216
openFiles,
217
};
218
};
219
220