Path: blob/main/src/project/project-environment.ts
6458 views
/*1* project-environment.ts2*3* Copyright (C) 2021-2023 Posit Software, PBC4*/56import { extname } from "../deno_ral/path.ts";7import { existsSync } from "../deno_ral/fs.ts";8import { projectType } from "../project/types/project-types.ts";9import {10kManuscriptType,11ResolvedManuscriptConfig,12} from "../project/types/manuscript/manuscript-types.ts";13import { isPdfOutput } from "../config/format.ts";14import { ProjectContext } from "../project/types.ts";15import { kLocalDevelopment, quartoConfig } from "../core/quarto.ts";16import { ProjectEnvironment } from "./project-environment-types.ts";17import { gitHubContext } from "../core/github.ts";18import { SemVer } from "semver/mod.ts";19import { QuartoEditor, QuartoTool } from "./project-environment-types.ts";20import { withRenderServices } from "../command/render/render-services.ts";21import { NotebookContext } from "../render/notebook/notebook-types.ts";2223const kDefaultContainerTitle = "Default Container";2425export const makeProjectEnvironmentMemoizer = (26notebookContext: NotebookContext,27) => {28let cachedEnv: ProjectEnvironment | undefined = undefined;29return async (project: ProjectContext) => {30if (cachedEnv) {31return Promise.resolve(cachedEnv);32} else {33cachedEnv = await computeProjectEnvironment(notebookContext, project);34return cachedEnv;35}36};37};3839export const computeProjectEnvironment = async (40notebookContext: NotebookContext,41context: ProjectContext,42) => {43// Get the quarto version44const version = quartoConfig.version();45const quarto = version === kLocalDevelopment46? "prerelease"47: new SemVer(version);4849// Compute the GitHub Context50const github = await gitHubContext(context.dir);5152const containerCtx: ProjectEnvironment = {53title: kDefaultContainerTitle,54engines: context.engines,55tools: [],56codeEnvironment: "vscode",57quarto,58environments: [],59openFiles: [],60envVars: {},61github,62};6364// Figure out the editor65const editorContext = projectEditor(context);66containerCtx.codeEnvironment = editorContext.editor;67containerCtx.openFiles.push(...editorContext.openFiles);6869// Determine the title70const title = context.config?.project.title;71if (title) {72containerCtx.title = title;73}7475// Determine what tools (if any) we should also install76const tools = await projectTools(notebookContext, context);77containerCtx.tools.push(...tools);7879// Determine environments80const envFiles = Object.keys(environmentCommands);81for (const envFile of envFiles) {82if (existsSync(envFile)) {83containerCtx.environments.push(envFile);84}85}8687return containerCtx;88};8990interface EnvironmentOptions {91restore?: string;92features?: Record<string, Record<string, unknown>>;93}9495const environmentCommands: Record<string, EnvironmentOptions> = {96// TODO: this needs to happen in correct directory post setup97// options(repos = c(REPO_NAME = "https://packagemanager.posit.co/cran/__linux__/jammy/latest"))98"renv.lock": {99restore: `Rscript -e 'renv::restore();'`,100},101"requirements.txt": {102restore: `python3 -m pip install -r requirements.txt`,103},104"DESCRIPTION": {105restore: `Rscript -e 'devtools::install_local(getwd())'`,106},107"install.R": {108restore: `Rscript install.R`,109},110"environment.yml": {111restore: "conda env create -f environment.yml",112features: {113"ghcr.io/devcontainers/features/conda:1": {114addCondaForge: true,115},116},117},118"PipFile": {},119"PipFile.lock": {},120"setup.py": {},121"Project.toml": {},122"REQUIRE": {},123};124125// Regex used to determine whether file contents will require the installation of Chromium126const kChromiumHint = /````*{mermaid}|{dot}/gm;127128const projectTools = async (129notebookContext: NotebookContext,130context: ProjectContext,131) => {132// Determine what tools (if any) we should also install133let tinytex = false;134let chromium = false;135136for (const input of context.files.input) {137if (!tinytex) {138// If we haven't yet found the need for tinytex,139// go ahead and look for PDF format. Once a single140// file needs, it we can stop looking141const formats = await withRenderServices(142notebookContext,143(services) => context.renderFormats(input, services, "all", context),144);145146const hasPdf = Object.values(formats).some((format) => {147return isPdfOutput(format.pandoc);148});149tinytex = hasPdf;150}151152// See if the file contains mermaid or graphviZ153if (!chromium) {154const contents = Deno.readTextFileSync(input);155if (contents.match(kChromiumHint)) {156chromium = true;157}158}159160if (tinytex && chromium) {161break;162}163}164165const tools: QuartoTool[] = [];166if (tinytex) {167tools.push("tinytex");168}169if (chromium) {170tools.push("chromium");171}172return tools;173};174175const projectEditor = (context: ProjectContext) => {176const qmdCodeTool = context.engines.includes("knitr") ? "rstudio" : "vscode";177const ipynbCodeTool = "jupyterlab";178179const openFiles: string[] = [];180let editor: QuartoEditor = qmdCodeTool;181182// Determine the code environment183// Special case manuscripts - the root article will drive the code environment184if (projectType(context.config?.project.type).type === kManuscriptType) {185// Choose the code environment based upon the engine and article file type186const manuscriptConfig = context.config187?.[kManuscriptType] as ResolvedManuscriptConfig;188if (extname(manuscriptConfig.article) === ".qmd") {189editor = qmdCodeTool;190} else {191editor = ipynbCodeTool;192}193194// Open the main article file195openFiles.push(manuscriptConfig.article);196} else {197// Count the ipynb vs qmds and use that as guideline198const exts: Record<string, number> = {};199const inputs = context.files.input;200for (const input of inputs) {201const ext = extname(input);202exts[ext] = (exts[ext] || 0) + 1;203}204205const qmdCount = exts[".qmd"] || 0;206const ipynbCount = exts[".ipynb"] || 0;207if (qmdCount >= ipynbCount) {208editor = qmdCodeTool;209} else {210editor = ipynbCodeTool;211}212}213return {214editor,215openFiles,216};217};218219220