UNPKG

@css-doodle/cli

Version:

Command-line tool for css-doodle to preview and generate images/videos

205 lines (182 loc) 6.48 kB
import fs from 'node:fs/promises'; import { setTimeout } from 'node:timers/promises'; import { extname } from 'node:path'; import puppeteer from 'puppeteer-core'; import { defaultAppArgs, getCssDoodleLib } from '../static.js'; import { resolveBrowser } from '../browser.js'; import { screenshot } from './screenshot.js'; import { screencast } from './screencast.js'; import { clearRow, log, question, style } from '../utils.js'; import process from 'node:process'; const supportedOutputFormat = { image: ['png', 'jpeg', 'webp'], video: ['mp4', 'webm', 'gif'], }; export async function render(code, options = {}) { const args = [ ...defaultAppArgs, '--start-maximized', ]; if (options.type === 'codepen' || options.type === 'webpage') { args.push('--ignore-certificate-errors'); } const executablePath = await resolveBrowser(options.quiet); const settings = { defaultViewport: null, executablePath, args, }; const browser = await puppeteer.launch(settings); try { const page = await browser.newPage(); switch (options.type) { case 'css': case 'cssd': case 'stdin': { if (options.type === 'stdin' && maybeHTML(code)) { await page.setContent(code, { waitUntil: 'networkidle0' }); } else { await page.setContent(buildHTML(code, await getCssDoodleLib())); } break; } case 'codepen': { if (!options.quiet) { log.info('fetching from CodePen...'); } await page.goto(code); const iframe = await page.$('iframe#result'); if (iframe) { const code = await page.evaluate((el) => el.getAttribute('srcdoc'), iframe); await page.setContent(code, { waitUntil: 'networkidle0' }); } else { throw new Error('read CodePen failed'); } if (!options.quiet) { clearRow(); } break; } case 'webpage': case 'html': { if (!options.quiet) { log.info(`opening ${code}...`); } await page.goto(code, { waitUntil: 'networkidle2', }); if (!options.quiet) { clearRow(); } break; } default: { throw new Error(`invalid type '${options.type}'`); } } const outputInfo = getOutputInfo(options); options.format = outputInfo.format; if (outputInfo.type === 'video' && !options.time) { options.time = 5 * 1000; } if (!options.title) { options.title = outputInfo.type === 'video' ? 'record' : 'image'; } if (!options.output) { let tag = Date.now(); if (/^(css|cssd)$/.test(options.type)) { tag = await page.$eval('css-doodle', (el) => el.seed); } options.output = `${options.title}-${tag}.${options.format}`; } const proceed = await processIfOutputExists(options.output, options.yes, options.quiet); if (!proceed) { log.warn('aborted'); return; } if (options.delay) { await setTimeout(options.delay); } return outputInfo.type === 'video' ? await screencast(page, options) : await screenshot(page, options); } finally { await browser.close(); } } async function processIfOutputExists(output, yes, quiet = false) { try { await fs.access(output, fs.constants.F_OK); if (yes) { return true; } // In non-TTY environments (e.g., CI), can't prompt - fail gracefully if (!process.stdout.isTTY) { if (!quiet) { log.error(`"${output}" already exists. Use -y/--yes to overwrite or choose a different output name.`); } return false; } const answer = await question(style.yellow(`"${output}" already exists. Overwrite? [Y/n] `)); clearRow(); const normalized = answer.trim().toLowerCase(); return normalized === '' || normalized === 'y' || normalized === 'yes'; } catch (_e) { return true; } } function getOutputInfo(options = {}) { let format = typeof options.format === 'string' ? options.format.toLowerCase() : ''; const output = options.output; const time = options.time; const allFormats = Object.values(supportedOutputFormat).flat(); if (format && !allFormats.includes(format)) { throw new Error( `unrecognized format '${format}', supported formats are: ${allFormats.join(', ')}`, ); } if (output) { let ext = extname(output).slice(1).toLowerCase(); if (ext && allFormats.includes(ext)) { if (format && format !== ext) { throw new Error(`conflicting format '${format}' and output extension '${ext}'`); } else { format = ext; } } else { ext = format || (time ? 'mp4' : 'png'); options.output += '.' + ext; } } if (!format) { format = time ? 'mp4' : 'png'; } const type = supportedOutputFormat.image.includes(format) ? 'image' : 'video'; if (type === 'image' && time) { log.warn('time option is ignored for image formats.'); } return { type, format, }; } function buildHTML(code, cssDoodleLib) { return `<!doctype html> <html> <head> <meta charset="utf-8"> <style> html, body, #container { margin: 0; padding: 0; width: 100%; height: 100% } body, #container { display: grid; place-items: center; } </style> <script>${cssDoodleLib}</script> </head> <body> <div id="container"> <css-doodle><template>${code}</template></css-doodle> </div> </body> </html>`; } function maybeHTML(str) { return /^\s*(<([a-z][a-z0-9-]*)\s*[^>]*\/?>|<![^>]+>|<\?[^>]+\?>)/i.test(str); } export { buildHTML, getOutputInfo, maybeHTML, processIfOutputExists };