import { getLeafLocations } from '@strudel/mini'; import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; let ast = parse(input, { ecmaVersion: 2022, allowAwaitOutsideFunction: true, locations: true, }); let miniLocations = []; const collectMiniLocations = (value, node) => { const leafLocs = getLeafLocations(`"${value}"`, node.start, input); miniLocations = miniLocations.concat(leafLocs); }; let widgets = []; walk(ast, { enter(node, parent /* , prop, index */) { if (isBackTickString(node, parent)) { const { quasis } = node; const { raw } = quasis[0].value; this.skip(); emitMiniLocations && collectMiniLocations(raw, node); return this.replace(miniWithLocation(raw, node)); } if (isStringWithDoubleQuotes(node)) { const { value } = node; this.skip(); emitMiniLocations && collectMiniLocations(value, node); return this.replace(miniWithLocation(value, node)); } if (isWidgetFunction(node)) { emitWidgets && widgets.push({ from: node.arguments[0].start, to: node.arguments[0].end, value: node.arguments[0].raw, // don't use value! min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, step: node.arguments[3]?.value, }); return this.replace(widgetWithLocation(node)); } if (isBareSamplesCall(node, parent)) { return this.replace(withAwait(node)); } }, leave(node, parent, prop, index) {}, }); const { body } = ast; if (!body?.[body.length - 1]?.expression) { throw new Error('unexpected ast format without body expression'); } // add return to last statement if (addReturn) { const { expression } = body[body.length - 1]; body[body.length - 1] = { type: 'ReturnStatement', argument: expression, }; } let output = escodegen.generate(ast); if (wrapAsync) { output = `(async ()=>{${output}})()`; } if (!emitMiniLocations) { return { output }; } return { output, miniLocations, widgets }; } function isStringWithDoubleQuotes(node, locations, code) { if (node.type !== 'Literal') { return false; } return node.raw[0] === '"'; } function isBackTickString(node, parent) { return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression'; } function miniWithLocation(value, node) { const { start: fromOffset } = node; return { type: 'CallExpression', callee: { type: 'Identifier', name: 'm', }, arguments: [ { type: 'Literal', value }, { type: 'Literal', value: fromOffset }, ], optional: false, }; } // these functions are connected to @strudel/codemirror -> slider.mjs // maybe someday there will be pluggable transpiler functions, then move this there function isWidgetFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } function widgetWithLocation(node) { const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ type: 'Literal', value: id, raw: id, }); node.callee.name = 'sliderWithID'; return node; } function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } function withAwait(node) { return { type: 'AwaitExpression', argument: node, }; }