UNPKG

myst-cli

Version:
226 lines (225 loc) 8.82 kB
import path from 'node:path'; import { createStore } from 'redux'; import { chalkLogger, LogLevel } from 'myst-cli-utils'; import latestVersion from 'latest-version'; import boxen from 'boxen'; import chalk from 'chalk'; import { HttpsProxyAgent } from 'https-proxy-agent'; import pLimit from 'p-limit'; import { Semaphore } from 'async-mutex'; import { cpus } from 'node:os'; import { findCurrentProjectAndLoad, findCurrentSiteAndLoad, reloadAllConfigsForCurrentSite, } from '../config.js'; import { loadPlugins } from '../plugins.js'; import { selectors } from '../store/index.js'; import { rootReducer } from '../store/reducers.js'; import version from '../version.js'; import { isWhiteLabelled } from '../utils/whiteLabelling.js'; import { KernelManager, ServerConnection, SessionManager } from '@jupyterlab/services'; import { launchJupyterServer } from 'myst-execute'; import { default as nodeFetch, Headers, Request, Response } from 'node-fetch'; // fetch polyfill for node<18 if (!globalThis.fetch) { globalThis.fetch = nodeFetch; globalThis.Headers = Headers; globalThis.Request = Request; globalThis.Response = Response; } const CONFIG_FILES = ['myst.yml', 'myst.yaml']; const DEFAULT_API_URL = 'https://api.mystmd.org'; const NPM_COMMAND = 'npm i -g mystmd@latest'; const PIP_COMMAND = 'pip install -U mystmd'; const LOCALHOSTS = ['localhost', '127.0.0.1', '::1']; function socialLink({ twitter, bsky }) { if (bsky) { return `Follow ${chalk.yellowBright(`@${bsky}`)} for updates!\nhttps://bsky.app/profile/${bsky}`; } if (twitter) { return `Follow ${chalk.yellowBright(`@${twitter}`)} for updates!\nhttps://x.com/${twitter}`; } return ''; } export function logUpdateAvailable({ current, latest, upgradeCommand, twitter, bsky, }) { return boxen(`Update available! ${chalk.dim(`v${current}`)} ≫ ${chalk.green.bold(`v${latest}`)}\n\nRun \`${chalk.cyanBright.bold(upgradeCommand)}\` to update.\n\n${socialLink({ bsky, twitter })}`, { padding: 1, margin: 1, borderColor: 'green', borderStyle: 'round', textAlignment: 'center', }); } export class Session { API_URL; configFiles; store; $logger; doiLimiter; executionSemaphore; proxyAgent; _shownUpgrade = false; _latestVersion; _jupyterSessionManagerPromise; get log() { return this.$logger; } constructor(opts = {}) { // use env variable if set this.API_URL = process.env.API_URL ?? DEFAULT_API_URL; // trailing slashes will cause issues this.API_URL = this.API_URL.replace(/\/+$/, ''); console.debug(`building myst-cli session with API URL: ${this.API_URL}`); this.configFiles = (opts.configFiles ? opts.configFiles : CONFIG_FILES).slice(); this.$logger = opts.logger ?? chalkLogger(LogLevel.info, process.cwd()); this.doiLimiter = opts.doiLimiter ?? pLimit(3); this.executionSemaphore = opts.executionSemaphore ?? new Semaphore(Math.max(1, cpus().length - 1)); const proxyUrl = process.env.HTTPS_PROXY; if (proxyUrl) this.proxyAgent = new HttpsProxyAgent(proxyUrl); this.store = createStore(rootReducer); // Allow the latest version to be loaded latestVersion('mystmd') .then((latest) => { this._latestVersion = latest; }) .catch(() => null); } showUpgradeNotice() { if (this._shownUpgrade || !this._latestVersion || version === this._latestVersion || isWhiteLabelled()) return; this.log.info(logUpdateAvailable({ current: version, latest: this._latestVersion, upgradeCommand: process.env.MYST_LANG === 'PYTHON' ? PIP_COMMAND : NPM_COMMAND, bsky: 'mystmd.org', })); this._shownUpgrade = true; } async reload() { await findCurrentProjectAndLoad(this, '.'); await findCurrentSiteAndLoad(this, '.'); if (selectors.selectCurrentSitePath(this.store.getState())) { await reloadAllConfigsForCurrentSite(this); } return this; } async fetch(url, init) { const urlOnly = new URL(url.url ?? url); this.log.debug(`Fetching: ${urlOnly}`); if (this.proxyAgent && !LOCALHOSTS.includes(urlOnly.hostname)) { if (!init) init = {}; init = { agent: this.proxyAgent, ...init }; this.log.debug(`Using HTTPS proxy: ${this.proxyAgent.proxy}`); } const logData = { url: urlOnly, done: false }; setTimeout(() => { if (!logData.done) this.log.info(`⏳ Waiting for response from ${url}`); }, 5000); const resp = await nodeFetch(url, init); logData.done = true; return resp; } plugins; async loadPlugins(plugins) { this.plugins = await loadPlugins(this, plugins); return this.plugins; } sourcePath() { const state = this.store.getState(); const sitePath = selectors.selectCurrentSitePath(state); const projectPath = selectors.selectCurrentProjectPath(state); const root = sitePath ?? projectPath ?? '.'; return path.resolve(root); } buildPath() { return path.join(this.sourcePath(), '_build'); } sitePath() { return path.join(this.buildPath(), 'site'); } contentPath() { return path.join(this.sitePath(), 'content'); } publicPath() { return path.join(this.sitePath(), 'public'); } _clones = []; async clone() { const cloneSession = new Session({ logger: this.log, doiLimiter: this.doiLimiter, executionSemaphore: this.executionSemaphore, configFiles: this.configFiles, }); await cloneSession.reload(); // TODO: clean this up through better state handling cloneSession._jupyterSessionManagerPromise = this._jupyterSessionManagerPromise; this._clones.push(cloneSession); return cloneSession; } getAllWarnings(ruleId) { const stringWarnings = []; const warnings = []; [this, ...this._clones].forEach((session) => { const sessionWarnings = selectors.selectFileWarningsByRule(session.store.getState(), ruleId); sessionWarnings.forEach((warning) => { const stringWarning = JSON.stringify(Object.entries(warning).sort()); if (!stringWarnings.includes(stringWarning)) { stringWarnings.push(stringWarning); warnings.push(warning); } }); }); return warnings; } jupyterSessionManager() { if (this._jupyterSessionManagerPromise === undefined) { this._jupyterSessionManagerPromise = this.createJupyterSessionManager(); } return this._jupyterSessionManagerPromise; } async createJupyterSessionManager() { try { let partialServerSettings; // Load from environment if (process.env.JUPYTER_BASE_URL !== undefined) { partialServerSettings = { baseUrl: process.env.JUPYTER_BASE_URL, token: process.env.JUPYTER_TOKEN, }; } else { // Note: To use an existing Jupyter server use `findExistingJupyterServer`, see #1716 this.log.debug(`Launching jupyter server on ${this.sourcePath()}`); // Create and load new server partialServerSettings = await launchJupyterServer(this.sourcePath(), this.log); } const serverSettings = ServerConnection.makeSettings(partialServerSettings); const kernelManager = new KernelManager({ serverSettings }); const manager = new SessionManager({ kernelManager, serverSettings }); // Tie the lifetime of the kernelManager and (potential) spawned server to the manager manager.disposed.connect(() => { kernelManager.dispose(); partialServerSettings?.dispose?.(); }); return manager; } catch (err) { this.log.error('Unable to instantiate connection to Jupyter Server', err); return undefined; } } dispose() { this._clones.forEach((session) => { session.dispose(); }); if (this._jupyterSessionManagerPromise) { this._jupyterSessionManagerPromise.then((manager) => manager?.dispose?.()); this._jupyterSessionManagerPromise = undefined; } } }