diff --git a/README.md b/README.md index f845f99..1622e89 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,9 @@ import BigNumber from './path/to/bignumber.mjs'; ![External import](docs/images/external_import.png) -Currently every external is loaded once and cached until Leporello is restarted (TODO what happens if we load modules in iframe and then recreate iframe) +Currently every external is loaded once and cached until Leporello is restarted +(TODO what happens if we load modules in iframe and then recreate iframe) +(TODO serve modules from service worker, change host every time) ## Hotkeys @@ -123,6 +125,10 @@ See built-in Help Editing local files is possible via [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API). Click "Allow access to local project folder" to grant access to local directory. +## Run and debug UI code in separate window + +TODO + ## Run Leporello locally To run it locally, you need to clone repo to local folder and serve it via HTTPS protocol (HTTPS is required by [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)). See [How to use HTTPS for local development](https://web.dev/how-to-use-local-https/) diff --git a/docs/examples/preact/index.js b/docs/examples/preact/index.js new file mode 100644 index 0000000..5a9ec7d --- /dev/null +++ b/docs/examples/preact/index.js @@ -0,0 +1,23 @@ +/* external */ +import {h, render} from 'https://unpkg.com/preact?module'; + +/* external */ +import {Stateful} from './stateful.js' + +const Counter = Stateful({ + getInitialState: () => ({counter: 0}), + + handlers: { + inc: ({counter}) => ({counter: counter + 1}), + dec: ({counter}) => ({counter: counter - 1}), + }, + + render: (props, state, handlers) => + h('div', null, + h('span', null, state.counter), + h('button', {onClick: handlers.inc}, 'Increment'), + h('button', {onClick: handlers.dec}, 'Decrement'), + ) +}) + +render(h(Counter), globalThis.document.body) diff --git a/docs/examples/preact/stateful.js b/docs/examples/preact/stateful.js new file mode 100644 index 0000000..3d835a9 --- /dev/null +++ b/docs/examples/preact/stateful.js @@ -0,0 +1,31 @@ +import {Component} from 'https://unpkg.com/preact?module'; + +export const Stateful = ({getInitialState, handlers, render}) => { + + return class extends Component { + + constructor() { + super() + this.compState = getInitialState() + this.handlers = Object.fromEntries( + Object + .entries(handlers) + .map(([name, h]) => + [name, this.makeHandler(h)] + ) + ) + } + + makeHandler(h) { + return (...args) => { + this.compState = h(this.compState, ...args) + this.forceUpdate() + } + } + + render() { + return render(this.props, this.compState, this.handlers) + } + } + +} diff --git a/src/calltree.js b/src/calltree.js index 8882b3f..237634e 100644 --- a/src/calltree.js +++ b/src/calltree.js @@ -447,10 +447,15 @@ nodes that are in the second tree that are not in the first tree */ const merge_calltrees = (prev, next) => { return Object.fromEntries( - Object.entries(prev).map(([module, {exports, calls}]) => + Object.entries(prev).map(([module, {is_external, exports, calls}]) => [ module, - {exports, calls: merge_calltree_nodes(calls, next[module].calls)[1]} + is_external + ? {is_external, exports} + : { + exports, + calls: merge_calltree_nodes(calls, next[module].calls)[1] + } ] ) ) diff --git a/src/cmd.js b/src/cmd.js index 0949701..b643c9e 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -71,6 +71,7 @@ const run_code = (s, index, dirty_files) => { ...s, parse_result, calltree: null, + async_calls: null, // Shows that calltree is brand new and requires entire rerender calltree_changed_token: {}, @@ -82,7 +83,6 @@ const run_code = (s, index, dirty_files) => { calltree_node_is_expanded: null, frames: null, calltree_node_by_loc: null, - // TODO keep selection_state? selection_state: null, loading_external_imports_state: null, } @@ -734,8 +734,10 @@ const move_cursor = (s, index) => { return do_move_cursor(state, index) } -const on_async_call = (state, ...args) => { - console.log('on_async_call', state, args) +const on_async_call = (state, call) => { + return {...state, + async_calls: [...(state.async_calls ?? []), call] + } } const load_dir = (state, dir) => { diff --git a/src/eval.js b/src/eval.js index 5fb4b4b..044ebde 100644 --- a/src/eval.js +++ b/src/eval.js @@ -391,7 +391,12 @@ export const eval_modules = ( is_toplevel_call = is_toplevel_call_copy if(is_recording_async_calls && is_toplevel_call) { - on_async_call(children) + if(children.length != 1) { + throw new Error('illegal state') + } + const call = children[0] + children = null + on_async_call(call) } } } @@ -542,13 +547,7 @@ export const eval_modules = ( , /* on_async_call */ - calls => { - on_async_call( - calls.map(c => - assign_code(modules, c) - ) - ) - }, + call => on_async_call(assign_code(modules, call)) ) const calltree_actions = { diff --git a/src/index.js b/src/index.js index 818d45c..d5fdd56 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,17 @@ const EXAMPLE = `const fib = n => : fib(n - 1) + fib(n - 2) fib(6)` + +// By default run code in hidden iframe, until user explicitly opens visible +// window +globalThis.run_window = (() => { + const iframe = document.createElement('iframe') + iframe.src = 'about:blank' + iframe.setAttribute('hidden', '') + document.body.appendChild(iframe) + return iframe.contentWindow +})() + export const open_run_window = () => { if(globalThis.run_window != null) { globalThis.run_window.close() diff --git a/test/test.js b/test/test.js index 0ee167d..fb285a1 100644 --- a/test/test.js +++ b/test/test.js @@ -2306,11 +2306,27 @@ const y = x()` // Use Function constructor to exec impure code for testing new Function('fn', 'globalThis.__run_async_call = fn')(fn) ` - const i = test_initial_state(code, { - on_async_call: (calls) => {console.log('test on async call', calls)} - }) - globalThis.__run_async_call() - //delete globalThis.__run_async_call + + const {get_async_call, on_async_call} = (new Function(` + let call + return { + get_async_call() { + return call + }, + on_async_call(_call) { + call = _call + } + } + `))() + + const i = test_initial_state(code, { on_async_call }) + globalThis.__run_async_call(10) + const call = get_async_call() + assert_equal(call.fn.name, 'fn') + assert_equal(call.code.index, code.indexOf('() => {')) + assert_equal(call.args, [10]) + const state = COMMANDS.on_async_call(i, call) + assert_equal(state.async_calls, [call]) }), ]