transpiler package to replace eval package
This commit is contained in:
parent
fada7fc9cc
commit
9b2899c8fb
7 changed files with 265 additions and 90 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import shapeshifter from '@strudel.cycles/eval/shapeshifter.mjs';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import tune from './tune.mjs';
|
||||
|
||||
const ctx = getAudioContext();
|
||||
|
|
@ -18,7 +18,7 @@ evalScope(
|
|||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
transpiler: shapeshifter,
|
||||
transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => {
|
||||
ctx.resume();
|
||||
|
|
|
|||
5
packages/transpiler/index.mjs
Normal file
5
packages/transpiler/index.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||
import { transpiler } from './transpiler.mjs';
|
||||
export * from './transpiler.mjs';
|
||||
|
||||
export const evaluate = (code) => _evaluate(code, transpiler);
|
||||
32
packages/transpiler/package.json
Normal file
32
packages/transpiler/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "@strudel.cycles/transpiler",
|
||||
"version": "0.3.1",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.3.1",
|
||||
"acorn": "^8.8.1",
|
||||
"escodegen": "^2.0.0",
|
||||
"estree-walker": "^3.0.1"
|
||||
}
|
||||
}
|
||||
39
packages/transpiler/test/transpiler.test.mjs
Normal file
39
packages/transpiler/test/transpiler.test.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
transpiler.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/transpiler/test/transpiler.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { transpiler } from '../transpiler.mjs';
|
||||
|
||||
describe('transpiler', () => {
|
||||
it('wraps double quote string with mini and adds location', () => {
|
||||
expect(transpiler('"c3"', { wrapAsync: false, addReturn: false })).toEqual("mini('c3').withMiniLocation(0, 4);");
|
||||
expect(transpiler('stack("c3","bd sd")', { wrapAsync: false, addReturn: false })).toEqual(
|
||||
"stack(mini('c3').withMiniLocation(6, 10), mini('bd sd').withMiniLocation(11, 18));",
|
||||
);
|
||||
});
|
||||
it('wraps backtick string with mini and adds location', () => {
|
||||
expect(transpiler('`c3`', { wrapAsync: false, addReturn: false })).toEqual("mini('c3').withMiniLocation(0, 4);");
|
||||
});
|
||||
it('replaces note variables with note strings', () => {
|
||||
expect(transpiler('seq(c3, d3)', { wrapAsync: false, addReturn: false })).toEqual("seq('c3', 'd3');");
|
||||
});
|
||||
it('keeps tagged template literal as is', () => {
|
||||
expect(transpiler('xxx`c3`', { wrapAsync: false, addReturn: false })).toEqual('xxx`c3`;');
|
||||
});
|
||||
it('supports top level await', () => {
|
||||
expect(transpiler("await samples('xxx');", { wrapAsync: false, addReturn: false })).toEqual(
|
||||
"await samples('xxx');",
|
||||
);
|
||||
});
|
||||
/* it('parses dynamic imports', () => {
|
||||
expect(
|
||||
transpiler("const { default: foo } = await import('https://bar.com/foo.js');", {
|
||||
wrapAsync: false,
|
||||
addReturn: false,
|
||||
}),
|
||||
).toEqual("const {default: foo} = await import('https://bar.com/foo.js');");
|
||||
}); */
|
||||
});
|
||||
100
packages/transpiler/transpiler.mjs
Normal file
100
packages/transpiler/transpiler.mjs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import escodegen from 'escodegen';
|
||||
import { parse } from 'acorn';
|
||||
import { walk } from 'estree-walker';
|
||||
import { isNote } from '@strudel.cycles/core';
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = true, addReturn = true } = options;
|
||||
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
allowAwaitOutsideFunction: true,
|
||||
});
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent, prop, index) {
|
||||
if (isBackTickString(node, parent)) {
|
||||
const { quasis, start, end } = node;
|
||||
const { raw } = quasis[0].value;
|
||||
this.skip();
|
||||
return this.replace(miniWithLocation(raw, start, end));
|
||||
}
|
||||
if (isStringWithDoubleQuotes(node)) {
|
||||
const { value, start, end } = node;
|
||||
this.skip();
|
||||
return this.replace(miniWithLocation(value, start, end));
|
||||
}
|
||||
if (node.type === 'Identifier' && isNote(node.name)) {
|
||||
this.skip();
|
||||
return this.replace({ type: 'Literal', value: node.name });
|
||||
}
|
||||
// TODO:
|
||||
},
|
||||
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,
|
||||
};
|
||||
}
|
||||
const output = escodegen.generate(ast);
|
||||
if (wrapAsync) {
|
||||
return `(async ()=>{${output}})()`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
const { raw, type } = node;
|
||||
if (type !== 'Literal') {
|
||||
return false;
|
||||
}
|
||||
return raw[0] === '"';
|
||||
}
|
||||
|
||||
function isBackTickString(node, parent) {
|
||||
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
||||
}
|
||||
|
||||
function miniWithLocation(value, start, end) {
|
||||
// with location
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'mini',
|
||||
},
|
||||
arguments: [{ type: 'Literal', value }],
|
||||
optional: false,
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'withMiniLocation',
|
||||
},
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
value: start,
|
||||
},
|
||||
{
|
||||
type: 'Literal',
|
||||
value: end,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue