UNPKG

@curvenote/cli

Version:
125 lines (124 loc) 5.36 kB
import fs from 'node:fs'; import pLimit from 'p-limit'; import { join, dirname, basename, extname } from 'node:path'; import { LogLevel, tic, isDirectory } from 'myst-cli-utils'; import { config, getRawFrontmatterFromFile, loadConfig, selectors, writeConfigs } from 'myst-cli'; import { projectFrontmatterFromDTO, saveAffiliations } from '../../frontmatter.js'; import { Project } from '../../models.js'; import { confirmOrExit } from '../../utils/index.js'; import { processOption, projectLogString } from '../utils.js'; import { projectToJupyterBook } from './jupyter-book/project.js'; import { oxaLinkToMarkdown } from './markdown.js'; import { oxaLinkToNotebook } from './notebook.js'; function logWithLevel(session, msg, level) { if (level === LogLevel.info) { session.log.info(msg); } else { session.log.debug(msg); } } /** * Pull content for a project on a path * * Errors if project config does not exist or no remote project url is specified. */ export async function pullProject(session, path, opts) { const state = session.store.getState(); const projectConfig = selectors.selectLocalProjectConfig(state, path); if (!projectConfig) throw Error(`Cannot pull project from ${path}: no project config`); if (!projectConfig.remote) throw Error(`Cannot pull project from ${path}: no remote project url`); const project = await new Project(session, projectConfig.remote).get(); saveAffiliations(session, project.data); const newFrontmatter = projectFrontmatterFromDTO(session, project.data); session.store.dispatch(config.actions.receiveProjectConfig({ path, ...projectConfig, ...newFrontmatter })); await writeConfigs(session, path); const toc = tic(); logWithLevel(session, `📥 Pulling ${projectLogString(project)} into ${path}`, opts?.level); await projectToJupyterBook(session, project.id, { ci: opts?.ci, path, writeConfig: false, createNotebookFrontmatter: true, titleOnlyInFrontmatter: true, keepOutputs: true, // Project frontmatter is kept sepatare in project config, above ignoreProjectFrontmatter: true, }); if (fs.existsSync(join(path, '_toc.yml'))) { logWithLevel(session, toc(`🚀 Pulled ${path} in %s`), opts?.level); } } /** * Pull content for all projects in the site config * * Errors if no site config is loaded in the state. */ export async function pullProjects(session, opts) { const state = session.store.getState(); const siteConfig = selectors.selectCurrentSiteConfig(state); if (!siteConfig) throw Error('Cannot pull projects: no site config'); const limit = pLimit(1); if (siteConfig.projects) { const projectsToPull = siteConfig.projects.filter((proj) => Boolean(proj.path)); await Promise.all(projectsToPull?.map(async (proj) => { return limit(async () => pullProject(session, proj.path, opts)); })); } } export async function pullDocument(session, file) { const frontmatter = await getRawFrontmatterFromFile(session, file); if (!frontmatter?.oxa) { throw new Error(`File ${file} does not have a "oxa" in the frontmatter.`); } switch (extname(file)) { case '.md': await oxaLinkToMarkdown(session, frontmatter.oxa, basename(file), { path: dirname(file) }); break; case '.ipynb': await oxaLinkToNotebook(session, frontmatter.oxa, basename(file), { path: dirname(file) }); break; default: throw new Error('Unrecognized extension to pull document.'); } } /** * Pull new project content from curvenote.com * * If this is called from a folder with an existing site configuration and * no other path is specified, all projects included in the site are pulled. * Otherwise, a single project on the specified path is pulled. * * Errors if site config has no projects or if project does not exist * on specified path. */ export async function pull(session, path, opts) { path = path || '.'; const processedOpts = processOption(opts); if (!fs.existsSync(path)) { throw new Error(`Invalid path: "${path}", it must be a folder or file accessible from the local directory`); } if (!isDirectory(path)) { await confirmOrExit(`Pulling will overwrite the file "${path}". Are you sure?`, processedOpts); await pullDocument(session, path); return; } // Site config is loaded on session init const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState()); if (path === '.' && siteConfig) { const numProjects = siteConfig.projects?.length; if (!numProjects) throw new Error('Your site configuration has no projects'); const plural = numProjects > 1 ? 's' : ''; await confirmOrExit(`Pulling will overwrite all content in ${numProjects} project${plural}. Are you sure?`, processedOpts); await pullProjects(session, { level: LogLevel.info, ...opts }); } else { await loadConfig(session, path); await confirmOrExit(`Pulling will overwrite all content in ${path === '.' ? 'current directory' : path}. Are you sure?`, processedOpts); await pullProject(session, path, { level: LogLevel.info, ...opts }); } }