UNPKG

@curvenote/cli

Version:
259 lines (258 loc) 10.2 kB
import { uuidv7 } from 'uuidv7'; import inquirer from 'inquirer'; import fs from 'fs/promises'; import { silentLogger } from 'myst-cli-utils'; import { ExportFormats } from 'myst-frontmatter'; import AdmZip from 'adm-zip'; import { selectors, writeConfigs, createTempFolder, buildSite, clean, collectAllBuildExportOptions, localArticleExport, runMecaExport, } from 'myst-cli'; import { join, relative, dirname } from 'node:path'; import * as uploads from '../uploads/index.js'; import { cleanProjectConfigForWrite } from '../sync/utils.js'; import chalk from 'chalk'; import { getFromJournals } from '../utils/api.js'; import { addTransformersToOpts } from '../utils/utils.js'; export const CDN_KEY_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; export const DEV_CDN_KEY = 'ad7fa60f-5460-4bf9-96ea-59be87944e41'; /** * Extract the source contents from a MECA export into a source/ folder */ async function createSourceFolder(session) { const activeLogger = session.$logger; session.$logger = silentLogger(); activeLogger.info('🎁 Bundling source files...'); try { const state = session.store.getState(); const projectPath = selectors.selectCurrentProjectPath(state); if (!projectPath) { activeLogger.debug('No project path found'); return; } const projectFile = selectors.selectLocalConfigFile(state, projectPath); if (!projectFile) { activeLogger.debug('No project file found'); return; } const mecaExport = { format: ExportFormats.meca, output: join(session.buildPath(), 'temp', 'meca-export.zip'), articles: [], }; await runMecaExport(session, projectFile, mecaExport, { projectPath }); const mecaZipPath = mecaExport.output; if (await fs .access(mecaZipPath) .then(() => false) .catch(() => true)) { activeLogger.debug('MECA export file not created'); return; } const zip = new AdmZip(mecaZipPath); // Extract the source files from MECA bundle folder const sourceEntries = zip .getEntries() .filter((entry) => entry.entryName.startsWith('bundle/') && entry.entryName !== 'bundle/'); if (sourceEntries.length === 0) { activeLogger.debug('No source files found in MECA export'); return; } const sourceFolderPath = join(session.sitePath(), 'source'); await fs.mkdir(sourceFolderPath, { recursive: true }); for (const entry of sourceEntries) { const relativePath = entry.entryName.replace(/^bundle\//, ''); const destPath = join(sourceFolderPath, ...relativePath.split('/')); // Ensure parent directory exists await fs.mkdir(dirname(destPath), { recursive: true }); if (!entry.isDirectory) { await fs.writeFile(destPath, entry.getData()); } } activeLogger.info(`🎁 Source folder created`); session.$logger = activeLogger; // Clean up the temporary MECA zip await fs.unlink(mecaZipPath); } catch (error) { activeLogger.debug(`Failed to create source folder: ${error}`); session.$logger = activeLogger; } } export async function performCleanRebuild(session, opts) { session.log.info('\n\n\t✨✨✨ performing a clean re-build of your work ✨✨✨\n\n'); await clean(session, [], { site: true, html: true, temp: true, exports: true, yes: true }); const exportOptionsList = await collectAllBuildExportOptions(session, [], { all: true }); const exportLogList = exportOptionsList.map((exportOptions) => { return `${relative('.', exportOptions.$file)} -> ${exportOptions.output}`; }); session.log.info(`📬 Performing exports:\n ${exportLogList.join('\n ')}`); await localArticleExport(session, exportOptionsList, {}); session.log.info(`⛴ Exports complete`); // Build the files in the content folder and process them await buildSite(session, addTransformersToOpts(session, opts ?? {})); // Create source folder from MECA export await createSourceFolder(session); session.log.info(`✅ Work rebuild complete`); } export async function uploadAndGetCdnKey(session, cdn, opts) { if (!process.env.DEV_CDN || process.env.DEV_CDN === 'false') { const uploadResult = await uploads.uploadToCdn(session, cdn, opts); return uploadResult.cdnKey; } if (process.env.DEV_CDN.match(CDN_KEY_RE)) { session.log.info(chalk.bold('Skipping upload, using DEV_CDN from environment')); return process.env.DEV_CDN; } session.log.info(chalk.bold('Skipping upload, using default DEV_CDN_KEY')); return DEV_CDN_KEY; } /** * Get project.id from the current config file * * The project.id will be used as journals work key * * If no config file is found this exits * If config file exists but project.id is not defined, * this returns undefined. */ export function workKeyFromConfig(session) { session.log.debug('Looking for key from config file'); const state = session.store.getState(); const projectConfigFile = selectors.selectCurrentProjectFile(state); if (!projectConfigFile) { session.log.error('No project configuration found'); process.exit(1); } session.log.debug(`Found config file: ${projectConfigFile}`); const projectConfig = selectors.selectCurrentProjectConfig(state); return projectConfig?.id; } export function workDoiFromConfig(session) { session.log.debug('Looking for doi from config file'); const state = session.store.getState(); const projectConfigFile = selectors.selectCurrentProjectFile(state); if (!projectConfigFile) { session.log.error('No project configuration found'); process.exit(1); } const projectConfig = selectors.selectCurrentProjectConfig(state); return projectConfig?.doi; } /** * Load work from transfer.yml data * * Returns undefined if work for the given venue is not defined or * if the API request for the work fails. */ export async function getMyWorkFromKey(session, key) { try { session.log.debug(`GET from journals API /my/works?key=${key}`); const resp = await getFromJournals(session, `/my/works?key=${key}`); return resp.items[0]; } catch { return undefined; } } export async function getMyWorksFromDoi(session, doi) { try { session.log.debug(`GET from journals API /my/works?doi=${doi}`); const resp = await getFromJournals(session, `/my/works?doi=${encodeURIComponent(doi)}`); return resp.items ?? []; } catch { return []; } } export async function workKeyExists(session, key) { try { const resp = await getFromJournals(session, `/works/key/${key}`); return !!resp?.exists; } catch { return false; } } export async function checkMyWorkAccess(session, key) { const owned = await getMyWorkFromKey(session, key); const taken = await workKeyExists(session, key); return { owned, taken }; } /** * Prompt user for a new work key * * First, gives a simple Y/n with a default UUID. If the user is unhappy with that, * they are prompted to write their own key. * * This key cannot already exist as a work key; if you want to link to an existing * work, you must put the key directly in your project config file. */ export async function promptForNewKey(session, opts) { const defaultKey = uuidv7(); if (opts?.yes) { session.log.debug(`Using autogenerated key: ${defaultKey}`); return defaultKey; } const { useDefault } = await inquirer.prompt([ { name: 'useDefault', message: `Work key is required. Use autogenerated value? (${defaultKey})`, type: 'confirm', default: true, }, ]); if (useDefault) return defaultKey; const { customKey } = await inquirer.prompt([ { name: 'customKey', type: 'input', message: 'Enter a unique key for your work?', validate: async (key) => { if (key.length < 8) { return 'Key must be at least 8 characters'; } if (key.length > 50) { return 'Key must be no more than 50 characters'; } try { const { exists } = await getFromJournals(session, `/works/key/${key}`); if (exists) return `Key "${key}" not available.`; } catch (err) { return 'Key validation failed'; } return true; }, }, ]); return customKey; } /** * Updates project.id in config yaml with key * * Creates a backup of the original file in the _build/temp folder */ export async function writeKeyToConfig(session, key) { const state = session.store.getState(); const path = selectors.selectCurrentProjectPath(state); const file = selectors.selectCurrentProjectFile(state); if (!file || !path) { session.log.error('No project configuration found'); process.exit(1); } const projectConfig = selectors.selectLocalProjectConfig(state, path); const tempFolder = createTempFolder(session); session.log.info(`creating backup copy of config file ${file} -> ${tempFolder}`); await fs.copyFile(file, join(tempFolder, 'curvenote.yml')); session.log.info(`writing work key to ${file}`); await writeConfigs(session, path, { projectConfig: cleanProjectConfigForWrite({ ...projectConfig, id: key }), }); } export function exitOnInvalidKeyOption(session, key) { session.log.debug(`Checking for valid key option: ${key}`); if (key.length < 8 || key.length > 128) { session.log.error(`⛔️ The key must be between 8 and 128 characters long.`); process.exit(1); } }