const {parse} = require("@babel/parser"); const generate = require("@babel/generator").default; const parserOptions = { allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowSuperOutsideMethod: true }; function hasFlag(name) { return process.env["WITH_" + name.toUpperCase()] === "1"; } function joinAndRenderTemplate(template) { return template.quasis.reduce((acc, el) => { acc += el.value.raw; const expr = template.expressions.shift(); if (typeof expr !== "undefined") { acc += generate(expr).code; } return acc; }, ''); } function replaceTempateExpressionWith(template, search, remplacement) { template.expressions = template.expressions.reduce((acc, expr) => { if (expr.name === search) { acc.push(remplacement); } else { acc.push(expr); } return acc; }, []); } function macro({types: t}) { const macroMap = {}; function renderMacro(originalMacro, args) { // args.reverse(); const macro = t.cloneDeep(originalMacro); const templateParams = macro.params; const template = macro.body; // replace in template templateParams.forEach((templateArg, index) => { const calledWithArg = args[index]; let remplacement = calledWithArg; if (typeof calledWithArg === "undefined") { remplacement = t.identifier("undefined"); } replaceTempateExpressionWith(template, templateArg.name, remplacement); }); // render return joinAndRenderTemplate(template); } function defineMacro(ident, fn, isDefault) { const {name} = ident; if (t.isArrowFunctionExpression(fn) === false) { throw new Error("Unsupported macro"); } if (name === "trace" && hasFlag("trace") === false) { return; } macroMap[name] = fn; macroMap[name].isDefault = isDefault; } /** * Default macros */ defineMacro( t.identifier("assert"), parse([ "(cond, msg) => `if (!(${cond})) {", "throw new Error('${cond}' + \" error: \" + (${msg} || \"unknown\"));", "}`" ].join("\n")).program.body[0].expression, true ); defineMacro( t.identifier("assertRuntimeError"), parse([ "(cond, msg) => `if (!(${cond})) {", "throw new RuntimeError('${cond}' + \" error: \" + (${msg} || \"unknown\"));", "}`" ].join("\n")).program.body[0].expression, true ); return { visitor: { Program(path, state) { if (typeof state.importedFromMamacro === "undefined") { state.importedFromMamacro = []; } }, /** * Collect maros to be processed */ ImportDeclaration(path, state) { const { source, specifiers } = path.node; if (t.isStringLiteral(source, { value: "mamacro" }) === false) { return; } specifiers.forEach(({ imported }) => state.importedFromMamacro.push(imported.name) ); path.remove(); }, /** * Process macros */ CallExpression(path, state) { const {node} = path; /** * Register macro */ const hasDefineBeenImported = state.importedFromMamacro.indexOf("define") !== -1; if ( hasDefineBeenImported === true && t.isIdentifier(node.callee, {name: "define"}) ) { defineMacro(node.arguments[0], node.arguments[1]); path.remove(); return; } /** * Process macro */ const macro = macroMap[node.callee.name]; const hasMacroBeenImported = state.importedFromMamacro.indexOf(node.callee.name); if (typeof macro !== "undefined") { // if it's a default macro and has not been imported, it's likely a // collision if ( macro.isDefault === true && state.importedFromMamacro.indexOf(node.callee.name) === -1 ) { return; } const callExpressionArgs = node.arguments; const string = renderMacro(macro, callExpressionArgs); const ast = parse(string, parserOptions).program.body; path.replaceWithMultiple(ast); } if (node.callee.name === "trace" && hasFlag("trace") === false) { path.remove(); } } } } } module.exports = macro;