@rise4fun/docusaurus-remark-plugin-compile-code
Version:
Run tool and show output.
423 lines • 17.8 kB
JavaScript
;
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