From 371af755df0c2f5501bfac587931ede8c8a0e678 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 6 Nov 2022 12:12:43 +0100 Subject: [PATCH] move evaluate logic without transpiler to core + breaking change: evalScope is now imported from core + breaking change: deprecated extend is now removed + add repl.mjs --- packages/core/evaluate.mjs | 45 +++++++++++++++++ packages/core/index.mjs | 1 + packages/core/repl.mjs | 23 +++++++++ packages/eval/README.md | 5 +- packages/eval/evaluate.mjs | 41 +--------------- packages/eval/test/evaluate.test.mjs | 4 +- packages/react/README.md | 3 +- packages/react/examples/nano-repl/src/App.jsx | 9 ++-- packages/react/src/App.jsx | 3 +- packages/react/src/hooks/useStrudel.mjs | 48 +++++++++---------- repl/package.json | 1 - repl/src/App.jsx | 4 +- repl/src/runtime.mjs | 2 +- tutorial/MiniRepl.jsx | 3 +- 14 files changed, 107 insertions(+), 85 deletions(-) create mode 100644 packages/core/evaluate.mjs create mode 100644 packages/core/repl.mjs diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs new file mode 100644 index 00000000..7e7fe1d1 --- /dev/null +++ b/packages/core/evaluate.mjs @@ -0,0 +1,45 @@ +/* +evaluate.mjs - +Copyright (C) 2022 Strudel contributors - see +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 . +*/ + +import * as strudel from '@strudel.cycles/core'; + +const { isPattern, Pattern } = strudel; + +let scoped = false; +export const evalScope = async (...args) => { + if (scoped) { + console.warn('evalScope was called more than once.'); + } + scoped = true; + const results = await Promise.allSettled(args); + const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); + results.forEach((result, i) => { + if (result.status === 'rejected') { + console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason); + } + }); + Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap()); +}; + +function safeEval(str) { + return Function('"use strict";return (' + str + ')')(); +} + +export const evaluate = async (code, transpiler) => { + if (!scoped) { + await evalScope(); // at least scope Pattern.prototype.boostrap + } + if (transpiler) { + code = transpiler(code); // transform syntactically correct js code to semantically usable code + } + let evaluated = await safeEval(code); + if (!isPattern(evaluated)) { + console.log('evaluated', evaluated); + const message = `got "${typeof evaluated}" instead of pattern`; + throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); + } + return { mode: 'javascript', pattern: evaluated }; +}; diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 2ef2eb19..c8139299 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -17,6 +17,7 @@ export * from './util.mjs'; export * from './speak.mjs'; export * from './clockworker.mjs'; export * from './scheduler.mjs'; +export * from './evaluate.mjs'; export { default as drawLine } from './drawLine.mjs'; export { default as gist } from './gist.js'; // below won't work with runtime.mjs (json import fails) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs new file mode 100644 index 00000000..591479e0 --- /dev/null +++ b/packages/core/repl.mjs @@ -0,0 +1,23 @@ +import { Cyclist } from './cyclist.mjs'; +import { evaluate as _evaluate } from './evaluate.mjs'; + +export function repl({ interval, defaultOutput, onSchedulerError, onEvalError, onEval, getTime, transpiler }) { + const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime }); + const evaluate = async (code) => { + if (!code) { + throw new Error('no code to evaluate'); + } + try { + const { pattern } = await _evaluate(code, transpiler); + scheduler.setPattern(pattern); + onEval({ + pattern, + code, + }); + } catch (err) { + onEvalError?.(err); + console.warn('eval error', err); + } + }; + return { scheduler, evaluate }; +} diff --git a/packages/eval/README.md b/packages/eval/README.md index a65558b0..466ae2f8 100644 --- a/packages/eval/README.md +++ b/packages/eval/README.md @@ -11,10 +11,9 @@ npm i @strudel.cycles/eval --save ## Example - - ```js -import { evaluate, extend } from '@strudel.cycles/eval'; +import { evalScope } from '@strudel.cycles/core'; +import { evaluate } from '@strudel.cycles/eval'; evalScope( import('@strudel.cycles/core'), diff --git a/packages/eval/evaluate.mjs b/packages/eval/evaluate.mjs index 3db93b6d..23132225 100644 --- a/packages/eval/evaluate.mjs +++ b/packages/eval/evaluate.mjs @@ -4,46 +4,9 @@ Copyright (C) 2022 Strudel contributors - see . */ +import { evaluate as _evaluate } from '@strudel.cycles/core'; import shapeshifter from './shapeshifter.mjs'; -import * as strudel from '@strudel.cycles/core'; - -const { isPattern, Pattern } = strudel; - -export const extend = (...args) => { - console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead'); - Object.assign(globalThis, ...args); -}; - -let scoped = false; -export const evalScope = async (...args) => { - if (scoped) { - console.warn('@strudel.cycles/eval evalScope was called more than once.'); - } - scoped = true; - const results = await Promise.allSettled(args); - const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); - results.forEach((result, i) => { - if (result.status === 'rejected') { - console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason); - } - }); - Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap()); -}; - -function safeEval(str) { - return Function('"use strict";return (' + str + ')')(); -} export const evaluate = async (code) => { - if (!scoped) { - await evalScope(); // at least scope Pattern.prototype.boostrap - } - const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code - let evaluated = await safeEval(shapeshifted); - if (!isPattern(evaluated)) { - console.log('evaluated', evaluated); - const message = `got "${typeof evaluated}" instead of pattern`; - throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); - } - return { mode: 'javascript', pattern: evaluated }; + return _evaluate(code, shapeshifter); }; diff --git a/packages/eval/test/evaluate.test.mjs b/packages/eval/test/evaluate.test.mjs index 11e973f4..4d7e3101 100644 --- a/packages/eval/test/evaluate.test.mjs +++ b/packages/eval/test/evaluate.test.mjs @@ -6,10 +6,10 @@ This program is free software: you can redistribute it and/or modify it under th import { expect, describe, it } from 'vitest'; -import { evaluate, evalScope } from '../evaluate.mjs'; +import { evaluate } from '../evaluate.mjs'; import { mini } from '@strudel.cycles/mini'; import * as strudel from '@strudel.cycles/core'; -const { fastcat } = strudel; +const { fastcat, evalScope } = strudel; describe('evaluate', async () => { await evalScope({ mini }, strudel); diff --git a/packages/react/README.md b/packages/react/README.md index ce2e5247..a03dbf4d 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -13,9 +13,8 @@ npm i @strudel.cycles/react Here is a minimal example of how to set up a MiniRepl: ```jsx -import { evalScope } from '@strudel.cycles/eval'; +import { evalScope, controls } from '@strudel.cycles/core'; import { MiniRepl } from '@strudel.cycles/react'; -import controls from '@strudel.cycles/core/controls.mjs'; import { prebake } from '../repl/src/prebake.mjs'; evalScope( diff --git a/packages/react/examples/nano-repl/src/App.jsx b/packages/react/examples/nano-repl/src/App.jsx index 769cb1ec..f82618af 100644 --- a/packages/react/examples/nano-repl/src/App.jsx +++ b/packages/react/examples/nano-repl/src/App.jsx @@ -1,5 +1,4 @@ -import controls from '@strudel.cycles/core/controls.mjs'; -import { evalScope } from '@strudel.cycles/eval'; +import { evalScope, controls } from '@strudel.cycles/core'; import { getAudioContext, panic, webaudioOutput } from '@strudel.cycles/webaudio'; import { useCallback, useState } from 'react'; import CodeMirror, { flash } from '../../../src/components/CodeMirror6'; @@ -94,7 +93,7 @@ function App() { if (e.code === 'Enter') { e.preventDefault(); flash(view); - await evaluate(); + await evaluate(code); if (e.shiftKey) { panic(); scheduler.stop(); @@ -120,7 +119,7 @@ function App() {
- {isDirty && } + {isDirty && }
{error &&

error {error.message}

} diff --git a/packages/react/src/App.jsx b/packages/react/src/App.jsx index f9542529..b22516da 100644 --- a/packages/react/src/App.jsx +++ b/packages/react/src/App.jsx @@ -1,8 +1,7 @@ import React from 'react'; import { MiniRepl } from './components/MiniRepl'; import 'tailwindcss/tailwind.css'; -import { evalScope } from '@strudel.cycles/eval'; -import { controls } from '@strudel.cycles/core'; +import { controls, evalScope } from '@strudel.cycles/core'; evalScope( controls, diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 250f3b51..571fb8e6 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -1,7 +1,7 @@ // import { Scheduler } from '@strudel.cycles/core'; import { useRef, useCallback, useEffect, useMemo, useState } from 'react'; -import { evaluate as _evaluate } from '@strudel.cycles/eval'; -import { Cyclist } from '@strudel.cycles/core/cyclist.mjs'; +import shapeshifter from '@strudel.cycles/eval/shapeshifter.mjs'; +import { repl } from '@strudel.cycles/core/repl.mjs'; function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) { // scheduler @@ -10,37 +10,33 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals const [activeCode, setActiveCode] = useState(code); const [pattern, setPattern] = useState(); const isDirty = code !== activeCode; - // TODO: how / when to remove schedulerError? - const scheduler = useMemo( - // () => new Scheduler({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }), - () => new Cyclist({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }), - [defaultOutput, interval], + const { scheduler, evaluate: _evaluate } = useMemo( + () => + repl({ + interval, + defaultOutput, + onSchedulerError: setSchedulerError, + onEvalError: setEvalError, + getTime, + transpiler: shapeshifter, + onEval: ({ pattern: _pattern, code }) => { + setActiveCode(code); + setPattern(_pattern); + setEvalError(); + }, + onEvalError: setEvalError, + }), + [defaultOutput, interval, getTime], ); - const evaluate = useCallback(async () => { - if (!code) { - console.log('no code..'); - return; - } - try { - // TODO: let user inject custom eval function? - const { pattern: _pattern } = await _evaluate(code); - setActiveCode(code); - scheduler?.setPattern(_pattern); - setPattern(_pattern); - setEvalError(); - } catch (err) { - setEvalError(err); - console.warn('eval error', err); - } - }, [code, scheduler]); + const evaluate = useCallback(() => _evaluate(code), [_evaluate, code]); const inited = useRef(); useEffect(() => { - if (!inited.current && evalOnMount) { + if (!inited.current && evalOnMount && code) { inited.current = true; evaluate(); } - }, [evaluate, evalOnMount]); + }, [evaluate, evalOnMount, code]); return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern }; } diff --git a/repl/package.json b/repl/package.json index 44858bb5..6f3c06a9 100644 --- a/repl/package.json +++ b/repl/package.json @@ -2,7 +2,6 @@ "name": "@strudel.cycles/repl", "private": true, "version": "0.0.0", - "type": "module", "scripts": { "dev": "vite --host", "start": "vite", diff --git a/repl/src/App.jsx b/repl/src/App.jsx index 7184289d..b91f43dc 100644 --- a/repl/src/App.jsx +++ b/repl/src/App.jsx @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { evalScope, evaluate } from '@strudel.cycles/eval'; +import { evaluate } from '@strudel.cycles/eval'; import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react'; import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; @@ -14,7 +14,7 @@ import * as tunes from './tunes.mjs'; import { prebake } from './prebake.mjs'; import * as WebDirt from 'WebDirt'; import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio'; -import { controls } from '@strudel.cycles/core'; +import { controls, evalScope } from '@strudel.cycles/core'; import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; diff --git a/repl/src/runtime.mjs b/repl/src/runtime.mjs index 7a2e60f0..1da561f9 100644 --- a/repl/src/runtime.mjs +++ b/repl/src/runtime.mjs @@ -4,7 +4,7 @@ // import * as tunes from './tunes.mjs'; import { evaluate } from '@strudel.cycles/eval'; -import { evalScope } from '@strudel.cycles/eval'; +import { evalScope } from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core'; import * as webaudio from '@strudel.cycles/webaudio'; import controls from '@strudel.cycles/core/controls.mjs'; diff --git a/tutorial/MiniRepl.jsx b/tutorial/MiniRepl.jsx index 840e6077..f7d32df5 100644 --- a/tutorial/MiniRepl.jsx +++ b/tutorial/MiniRepl.jsx @@ -1,6 +1,5 @@ -import { evalScope } from '@strudel.cycles/eval'; +import { evalScope, controls } from '@strudel.cycles/core'; import { MiniRepl as _MiniRepl } from '@strudel.cycles/react'; -import controls from '@strudel.cycles/core/controls.mjs'; import { samples } from '@strudel.cycles/webaudio'; import { prebake } from '../repl/src/prebake.mjs';