UNPKG

@rise4fun/docusaurus-remark-plugin-compile-code

Version:
423 lines 17.8 kB
"use strict"; const tslib_1 = require("tslib"); /// <refere const unist_util_visit_1 = tslib_1.__importDefault(require("unist-util-visit")); const fs_extra_1 = require("fs-extra"); const node_path_1 = require("node:path"); const hash_1 = tslib_1.__importDefault(require("./hash")); const child_process_1 = require("child_process"); const minimatch_1 = require("minimatch"); const puppeteer_1 = tslib_1.__importDefault(require("puppeteer")); const RESULT_FILE = 'result.json'; function readCachedResult(cwd) { const isProd = process.env.NODE_ENV === 'production'; if (!isProd) return undefined; // cache lookup const cached = (0, node_path_1.join)(cwd, RESULT_FILE); if ((0, fs_extra_1.existsSync)(cached)) { try { const res = (0, fs_extra_1.readJSONSync)(cached); return res; } catch (e) { // invalid file, delete folder (0, fs_extra_1.removeSync)(cwd); } } return undefined; } function parseMeta(meta = '') { const skip = /\s?skip|no-build\s?/i.test(meta); const ignoreErrors = /\s?ignore-?errors\s?/i.test(meta); return { skip, ignoreErrors }; } const BUILD_PATH = './.docusaurus/docusaurus-remark-plugin-compile-code/'; const cachePath = `${BUILD_PATH}cache/`; const outputPath = `${BUILD_PATH}src/`; const assetsPath = `${BUILD_PATH}assets/`; const plugin = (options = undefined) => { const { langs = [], cache = !process.env.RISE_COMPILE_CODE_NO_CACHE, failFast } = options || {}; let puppets = {}; const cleanupPuppets = async () => { Object.keys(puppets).forEach((k) => { const { close, pendingRequests } = puppets[k] || {}; if (pendingRequests && !Object.keys(pendingRequests).length) { console.debug(`${k}:puppet> cleanup`); close?.(); delete puppets[k]; } }); puppets = {}; }; const puppeteerCodeNoCache = async (cwd, source, meta, langOptions, hash) => { const { timeout = 60000 } = langOptions; let { page, pendingRequests } = puppets[langOptions.lang] || {}; if (!page) { pendingRequests = {}; const browser = await puppeteer_1.default.launch({ headless: 'new' }); const puppeteerVersion = await browser.version(); const msgp = `${langOptions.lang}:driver> `; console.info(`${msgp}starting browser ${puppeteerVersion}`); page = await browser.newPage(); if (!page) throw Error('page could not load'); page.on('console', (msg) => { console.log(msg.text()); }); const close = async () => { await page?.close(); await browser?.close(); }; let html = (langOptions.html && (0, fs_extra_1.readFileSync)(langOptions.html, { encoding: 'utf-8' })) || langOptions.createDriverHtml?.(langOptions); // exapnd scripts const scriptPromises = []; html = html?.replace(/<script src="file:\/\/([^"]+)">\s*<\/script>/gi, (m, n) => { console.debug(`inlining ${n}`); const js = (0, fs_extra_1.readFileSync)(n, { encoding: 'utf-8' }); if (!page) return 'missing page'; scriptPromises.push(page .addScriptTag({ content: js }) .then(() => { console.debug(`inlined ${n}`); }) .catch((e) => { console.error(e); throw e; })); return `<!-- inlined ${n} -->`; }); await Promise.all(scriptPromises); await page.exposeFunction('rise4funPostMessage', (msg) => { const resp = langOptions.resolveCompileResponse?.(msg) || msg; const id = resp?.id; const { resolve } = pendingRequests?.[id] || {}; if (resolve) { console.debug(`${msgp}received ${id}`); delete pendingRequests?.[id]; resolve({ ...resp, outputFiles: { 'driver.html': html, ...(resp.outputFiles || {}), }, }); } }); let readyResolve = undefined; let readyReject = undefined; const ready = new Promise(async (resolve, reject) => { readyResolve = resolve; readyReject = reject; }); await page?.exposeFunction('rise4funReady', () => { readyReject = undefined; readyResolve?.(); }); await page.setContent(html); console.debug(`${msgp}waiting browser`); let readyTimeoutId = setTimeout(() => { console.error(`${msgp}browser timeout`); readyResolve = undefined; readyReject?.({ message: 'browser timeout', outputFiles: { 'driver.html': html }, }); }, 30000); await ready; clearTimeout(readyTimeoutId); // wait for ready message puppets[langOptions.lang] = { page, pendingRequests, close }; } // send and wait for message const id = langOptions.lang + Math.random().toString(); const request = { id, type: 'puppet', lang: langOptions.lang, source, options: { ...langOptions, meta, cwd, }, }; const msg = langOptions.createCompileRequest?.(request) || request; const processing = new Promise((resolve, reject) => { console.debug(`${langOptions.lang}:puppeteer> schedule ${id}`); pendingRequests[id] = { resolve, reject }; page.evaluate(async (msg) => { window.postMessage(msg, '*'); }, msg); }); // concurrent timeout setTimeout(() => { const req = pendingRequests?.[id]; if (req) { console.error(`${langOptions.lang}:puppeteer> timeout ${id}`); delete pendingRequests?.[id]; req?.reject?.('render timeout'); } }, timeout); return processing; }; async function compileCode(cwd, source, meta, langOptions, cache, hash) { let result = cache && readCachedResult(cwd); if (result) return result; const { prefix } = langOptions; const psource = !prefix || source.indexOf(prefix) > -1 ? source : prefix + '\n\n' + source; (0, fs_extra_1.ensureDirSync)(cwd); result = await compileCodeNodeCache(cwd, psource, meta, langOptions, hash); // cache on disk if (result && cache) { (0, fs_extra_1.writeJSONSync)((0, node_path_1.join)(cwd, RESULT_FILE), result, { spaces: 2 }); } return result; } const compileCodeNodeCache = async (cwd, source, meta, langOptions, hash) => { const { timeout, lang, outputFiles = [] } = langOptions; const writeGeneratedOutputFiles = (res) => { const ros = res?.outputFiles || {}; Object.keys(ros).forEach((fn) => { const content = ros[fn]; console.debug(`write ${(0, node_path_1.join)(cwd, fn)}`); if (typeof content === 'string') (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(cwd, fn), content, { encoding: 'utf-8', }); else (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(cwd, fn), content); }); }; const cleanOutputFiles = (cwd) => { outputFiles?.forEach((f) => { try { (0, fs_extra_1.removeSync)((0, node_path_1.join)(cwd, f.name)); } catch (e) { console.debug(e); } }); }; const generateNodesFromOutputFiles = (result) => { if (outputFiles) { result.nodes = [ ...(result.nodes || []), ...outputFiles .filter(({ name }) => (0, fs_extra_1.existsSync)((0, node_path_1.join)(cwd, name))) .map(({ name, title, lang: outputLang, meta }) => { const fn = name; if (/\.(svg|png|jpg|jpeg)$/i.test(fn)) { // copy file to static folder const snd = (0, node_path_1.join)(assetsPath, lang, hash); (0, fs_extra_1.ensureDirSync)(snd); (0, fs_extra_1.copyFileSync)((0, node_path_1.join)(cwd, fn), (0, node_path_1.join)(snd, fn)); return { type: 'image', alt: title || fn, url: `/${(0, node_path_1.join)(lang, hash, fn)}`, }; } else { const text = (0, fs_extra_1.readFileSync)((0, node_path_1.join)(cwd, fn), { encoding: 'utf-8', }); return { type: 'code', lang: outputLang || (0, node_path_1.extname)(fn).slice(1), meta: `tabs title=${JSON.stringify(title || fn)} ${meta || ''}`, value: text, }; } }), ]; } }; // custom function const { compile } = langOptions; if (compile) { try { const res = await compile(source, { ...langOptions, meta, cwd, }); if (res) { writeGeneratedOutputFiles(res); generateNodesFromOutputFiles(res); } return res; } catch (e) { return { error: e + '', }; } } // puppeteer const { html, createDriverHtml } = langOptions; if (!!createDriverHtml || !!html) { try { const res = await puppeteerCodeNoCache(cwd, source, meta, langOptions, hash); if (res) { writeGeneratedOutputFiles(res); generateNodesFromOutputFiles(res); } return res; } catch (e) { return { error: e + '', }; } } // Tool const { extension, command, nodeBin, args = [], successReturnCode = 0, ignoreReturnCode, inputFiles = {}, } = langOptions; if (command || nodeBin) { const ifn = `input.${extension || lang}`; const iargs = [...args]; (0, fs_extra_1.ensureDirSync)(cwd); (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(cwd, ifn), source); // write prefiles Object.entries(inputFiles).map(([name, content]) => (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(cwd, name), typeof content === 'object' ? JSON.stringify(content, null, 2) : content, { encoding: 'utf-8' })); (0, fs_extra_1.writeJSONSync)((0, node_path_1.join)(cwd, 'options.json'), { ...langOptions, meta, }); // compile tool let cmd = command || ''; if (nodeBin) { if (process.platform === 'win32') { cmd = (0, node_path_1.resolve)((0, node_path_1.join)('node_modules', '.bin', nodeBin + '.cmd')); } else { iargs.unshift((0, node_path_1.resolve)((0, node_path_1.join)('node_modules', '.bin', nodeBin))); cmd = 'node'; } } else if (/\.m?js/.test(cmd)) { iargs.unshift((0, node_path_1.resolve)(cmd)); cmd = 'node'; } (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(cwd, `run.${process.platform === 'win32' ? 'cmd' : 'sh'}`), `${cmd} ${iargs.join(' ')}`); try { cleanOutputFiles(cwd); const res = (0, child_process_1.spawnSync)(cmd, iargs, { timeout, cwd, }); let error = res.error?.message || ''; if (!ignoreReturnCode && res.status !== successReturnCode && !res.stderr) error += `\exit code: ${res.status}`; const result = { stdout: res.stdout?.toString() || '', stderr: res.stderr?.toString() || '', error, }; generateNodesFromOutputFiles(result); return result; } catch (e) { return { stderr: e + '', error: 'tool execution failed', }; } } // unknown configuration return { error: `invalid configuration (${langOptions.lang})`, }; }; return async (root, vfile) => { let snippet = 0; const { history } = vfile; const fpath = vfile.path || history[history.length - 1] || 'unknown'; const fbase = (0, node_path_1.basename)(fpath); let errors = 0; const visited = new Set(); // visit called twice on async const todo = []; // collect all nodes (0, unist_util_visit_1.default)(root, 'code', (node, _, parent) => { if (!visited.has(node)) { visited.add(node); todo.push({ node, parent }); } }); // render for (const { node, parent } of todo) { if (!parent) continue; const { lang, meta, value } = node; if (!lang) return; const langOptions = langs.find((o) => o.lang === lang && (!o.meta || (meta || '').indexOf(o.meta) > -1)); if (!langOptions) continue; const { skip, ignoreErrors: ignoreErrorsMeta } = parseMeta(meta || ''); if (skip) continue; const { outputMeta = '', errorMeta = '', outputLang, errorLang, inputLang, ignoreErrors: ignoreErrorsLang, excludedFiles, } = langOptions; if (vfile.path !== undefined && excludedFiles?.some((excluded) => (0, minimatch_1.minimatch)(vfile.path || '', excluded))) { continue; } // save source in tree snippet++; const fn = (0, node_path_1.join)(outputPath, fbase, `code${snippet}.${lang}`); (0, fs_extra_1.ensureDirSync)((0, node_path_1.dirname)(fn)); (0, fs_extra_1.writeFileSync)(fn, value, { encoding: 'utf-8' }); // compute output let nextIndex = parent.children.indexOf(node) + 1; const ignoreErrors = ignoreErrorsLang || ignoreErrorsMeta; const hash = (0, hash_1.default)(value, meta || '', langOptions); const cwd = (0, node_path_1.join)(cachePath, lang, hash); const res = await compileCode(cwd, value, meta || '', langOptions, cache, hash); const { stdout, stderr, error, nodes } = res || {}; const out = [stdout?.trimEnd()].filter((s) => !!s); const err = [stderr?.trimEnd(), error].filter((s) => !!s); if (inputLang === null) { // remove code nextIndex--; parent.children.splice(nextIndex, 1); } else if (inputLang) node.lang = inputLang; if (outputLang !== null && out?.length) { parent.children.splice(nextIndex++, 0, { type: 'code', lang: outputLang, meta: outputMeta + ` title="Output"`, value: out.join('\n'), }); } if (errorMeta !== null && err?.length) { parent.children.splice(nextIndex++, 0, { type: 'code', lang: errorLang || 'console', meta: outputMeta + ` title="Error"`, value: err.join('\n'), }); } if (nodes?.length) { parent.children.splice(nextIndex, 0, ...nodes); nextIndex += nodes.length; } if (!ignoreErrors && res && (res?.error || stderr)) { errors++; console.error(`error ${vfile.path}: ${res.error || ''}`); if (failFast) throw new Error('error while compiling code snippet'); } } // cleanup pupppets await cleanupPuppets(); if (errors) throw new Error('errors while compile code snippets'); }; }; module.exports = plugin; //# sourceMappingURL=index.js.map