├── plugins ├── sublime │ ├── .no-sublime-package │ ├── .python-version │ ├── Replete.sublime-settings │ ├── Main.sublime-menu │ ├── Default.sublime-keymap │ ├── Default.sublime-commands │ ├── README.md │ └── replete.py ├── vscode │ ├── r2d2.png │ ├── .vscode │ │ └── launch.json │ ├── README.md │ ├── package.json │ └── extension.js ├── neovim │ ├── plugin │ │ └── neoreplete.vim │ ├── README.md │ └── lua │ │ ├── replete.lua │ │ └── json.lua ├── emacs │ ├── README.md │ └── replete.el ├── nrepl │ ├── README.md │ ├── bencode.js │ └── server.js └── mcp │ ├── README.md │ └── methodology.md ├── import_map.json ├── package.json ├── tjs_repl.js ├── bun_repl.js ├── node_loader.js ├── webl ├── webl_relay.js ├── README.md ├── r2d2.svg ├── webl_client.js ├── webl_inspect.js ├── c3po.svg ├── websocketify.js └── webl_server.js ├── node_repl.js ├── deno_repl.js ├── tjs_padawan.js ├── deno_padawan.js ├── cmdl_repl.js ├── fileify.js ├── node_padawan.js ├── replete.js ├── run.js ├── browser_repl.js ├── make.js ├── cmdl.js └── node_resolve.js /plugins/sublime/.no-sublime-package: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/sublime/.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /plugins/vscode/r2d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesdiacono/Replete/HEAD/plugins/vscode/r2d2.png -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "acorn": "npm:acorn", 4 | "acorn-walk": "npm:acorn-walk" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "acorn": "latest", 5 | "acorn-walk": "latest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /plugins/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /plugins/sublime/Replete.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "command": [ 3 | "deno", 4 | "run", 5 | "--allow-all", 6 | "--importmap", 7 | "https://deno.land/x/replete/import_map.json", 8 | "https://deno.land/x/replete/replete.js", 9 | "--browser_port=9325", 10 | "--content_type=js:text/javascript", 11 | "--content_type=mjs:text/javascript", 12 | "--content_type=map:application/json", 13 | "--content_type=css:text/css", 14 | "--content_type=html:text/html; charset=utf-8", 15 | "--content_type=wasm:application/wasm", 16 | "--content_type=woff2:font/woff2", 17 | "--content_type=svg:image/svg+xml", 18 | "--content_type=png:image/png", 19 | "--content_type=webp:image/webp" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /plugins/sublime/Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "preferences", 3 | "children": [{ 4 | "id": "package-settings", 5 | "children": [{ 6 | "caption": "Replete", 7 | "children": [ 8 | { 9 | "caption": "Settings", 10 | "command": "edit_settings", 11 | "args": { 12 | "base_file": "${packages}/Replete/Replete.sublime-settings", 13 | "default": "{}" 14 | }, 15 | }, 16 | { 17 | "caption": "Key Bindings", 18 | "command": "edit_settings", 19 | "args": { 20 | "base_file": "${packages}/Replete/Default.sublime-keymap", 21 | "default": "[]" 22 | }, 23 | }, 24 | ] 25 | }] 26 | }] 27 | }] 28 | -------------------------------------------------------------------------------- /plugins/sublime/Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+shift+r"], 4 | "command": "replete" 5 | }, 6 | { 7 | "keys": ["ctrl+alt+shift+l"], 8 | "command": "replete_clear" 9 | }, 10 | { 11 | "keys": ["ctrl+alt+shift+b"], 12 | "command": "replete_send", 13 | "args": {"platform": "browser"} 14 | }, 15 | { 16 | "keys": ["ctrl+alt+shift+n"], 17 | "command": "replete_send", 18 | "args": {"platform": "node"} 19 | }, 20 | { 21 | "keys": ["ctrl+alt+shift+d"], 22 | "command": "replete_send", 23 | "args": {"platform": "deno"} 24 | }, 25 | { 26 | "keys": ["ctrl+alt+shift+u"], 27 | "command": "replete_send", 28 | "args": {"platform": "bun"} 29 | }, 30 | { 31 | "keys": ["ctrl+alt+shift+t"], 32 | "command": "replete_send", 33 | "args": {"platform": "tjs"} 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /tjs_repl.js: -------------------------------------------------------------------------------- 1 | // This REPL evaluates JavaScript in a Txiki process. 2 | 3 | /*jslint node */ 4 | 5 | import url from "node:url"; 6 | import make_cmdl_repl from "./cmdl_repl.js"; 7 | import fileify from "./fileify.js"; 8 | const padawan_url = new URL("./tjs_padawan.js", import.meta.url); 9 | 10 | function make_tjs_repl(capabilities, which, args = [], env = {}) { 11 | return make_cmdl_repl( 12 | capabilities, 13 | function make_command(tcp_host) { 14 | 15 | // Txiki is not capable of loading a program over HTTP, even though it can 16 | // import modules over HTTP. 17 | 18 | return fileify(padawan_url).then(function (padawan_file_url) { 19 | return [ 20 | which, 21 | ...args, 22 | "run", 23 | url.fileURLToPath(padawan_file_url), 24 | tcp_host 25 | ]; 26 | }); 27 | }, 28 | env 29 | ); 30 | } 31 | 32 | export default Object.freeze(make_tjs_repl); 33 | -------------------------------------------------------------------------------- /plugins/neovim/plugin/neoreplete.vim: -------------------------------------------------------------------------------- 1 | " This is the entrypoint to the plugin. It defers to a Lua module, which does 2 | " all the real work. 3 | 4 | lua replete = require("replete") 5 | 6 | " Define some keyboard shortcuts. The "eval" shortcuts are available in both 7 | " visual and non-visual mode, so that you can use them repetitively. 8 | 9 | command Replete normal! :lua replete.toggle() 10 | nmap :lua replete.eval("browser") 11 | nmap :lua replete.eval("node") 12 | nmap :lua replete.eval("deno") 13 | nmap :lua replete.eval("bun") 14 | nmap :lua replete.eval("tjs") 15 | vnoremap :lua replete.eval("browser") 16 | vnoremap :lua replete.eval("node") 17 | vnoremap :lua replete.eval("deno") 18 | vnoremap :lua replete.eval("bun") 19 | vnoremap :lua replete.eval("tjs") 20 | 21 | " If we neglect to kill the Replete process, then it outlives nvim. Register an 22 | " event handler to be called just before nvim exits, killing Replete. 23 | 24 | autocmd VimLeavePre * :lua replete.stop() 25 | -------------------------------------------------------------------------------- /plugins/sublime/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Replete: Start", 4 | "command": "replete" 5 | }, 6 | { 7 | "caption": "Replete: Clear output", 8 | "command": "replete_clear" 9 | }, 10 | { 11 | "caption": "Replete: Evaluate selection in the browser", 12 | "command": "replete_send", 13 | "args": {"platform": "browser"} 14 | }, 15 | { 16 | "caption": "Replete: Evaluate selection in Node.js", 17 | "command": "replete_send", 18 | "args": {"platform": "node"} 19 | }, 20 | { 21 | "caption": "Replete: Evaluate selection in Deno", 22 | "command": "replete_send", 23 | "args": {"platform": "deno"} 24 | }, 25 | { 26 | "caption": "Replete: Evaluate selection in Bun", 27 | "command": "replete_send", 28 | "args": {"platform": "bun"} 29 | }, 30 | { 31 | "caption": "Replete: Evaluate selection in Txiki", 32 | "command": "replete_send", 33 | "args": {"platform": "tjs"} 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /bun_repl.js: -------------------------------------------------------------------------------- 1 | // This REPL evaluates JavaScript in a Bun process. 2 | 3 | /*jslint node */ 4 | 5 | import url from "node:url"; 6 | import make_cmdl_repl from "./cmdl_repl.js"; 7 | import fileify from "./fileify.js"; 8 | const padawan_url = new URL("./node_padawan.js", import.meta.url); 9 | 10 | function make_bun_repl(capabilities, which, args = [], env = {}) { 11 | return make_cmdl_repl( 12 | capabilities, 13 | function make_command(tcp_host) { 14 | 15 | // Make sure we have predownloaded the padawan script, necessary until Bun 16 | // supports loading programs over HTTP. 17 | // Pending https://github.com/oven-sh/bun/issues/38. 18 | 19 | return fileify(padawan_url).then(function (padawan_file_url) { 20 | return [ 21 | which, 22 | "run", 23 | ...args, 24 | url.fileURLToPath(padawan_file_url.href), 25 | tcp_host 26 | ]; 27 | }); 28 | }, 29 | env 30 | ); 31 | } 32 | 33 | export default Object.freeze(make_bun_repl); 34 | -------------------------------------------------------------------------------- /plugins/emacs/README.md: -------------------------------------------------------------------------------- 1 | # Replete Emacs plugin 2 | 3 | This is an Emacs plugin for Replete, a multi-platform JavaScript REPL. 4 | 5 | The source code derives from [Skerrick](https://github.com/anonimitoraf/skerrick), making it subject to the GPL v3.0 licence. 6 | 7 | ## Installation 8 | 9 | Install [Deno](https://deno.com), ensuring `deno` is in your `PATH`, then run `(load-file "/path/to/replete.el")` (adjusting the path as necessary). 10 | 11 | ## Usage 12 | 13 | M-x replete-start 14 | 15 | Starts (or restarts) the Replete process. Run this before you try to evaluate anything. Whilst Replete is running, you can monitor its output in the *replete* buffer. 16 | 17 | You will be prompted for the directory in which to start Replete. This directory will be checked for a _replete.json_ file. By default, Replete will not let you import modules located outside of this directory. 18 | 19 | M-x replete-stop 20 | 21 | Kills the Replete process and closes the *replete* buffer. 22 | 23 | M-x replete-browser 24 | M-x replete-node 25 | M-x replete-deno 26 | M-x replete-bun 27 | M-x replete-tjs 28 | 29 | Evaluates the selected region of your buffer. The result will appear in the *replete* buffer. 30 | 31 | Configuration options are described [here](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#configuration). 32 | -------------------------------------------------------------------------------- /node_loader.js: -------------------------------------------------------------------------------- 1 | // A Node.js "loader" module that imbues a Node.js process with the ability to 2 | // import modules over HTTP. 3 | 4 | // Unlike the --experimental-network-imports flag, this loader permits modules 5 | // imported over the network to import built-in modules such as "node:fs", and 6 | // it ignores CORS. 7 | 8 | /*jslint node */ 9 | 10 | const rx_http = /^https?:\/\//; 11 | 12 | function resolve(specifier, context, next_resolve) { 13 | if (rx_http.test(specifier)) { 14 | return { 15 | url: specifier, 16 | shortCircuit: true 17 | }; 18 | } 19 | if (context.parentURL && rx_http.test(context.parentURL)) { 20 | return { 21 | url: new URL(specifier, context.parentURL).href, 22 | shortCircuit: true 23 | }; 24 | } 25 | return next_resolve(specifier, context); 26 | } 27 | 28 | function load(url, context, next_load) { 29 | if (rx_http.test(url)) { 30 | 31 | // Load the module's source code from the network. 32 | 33 | return fetch(url).then(function (response) { 34 | if (!response.ok) { 35 | throw new Error("Failed to load " + url + "."); 36 | } 37 | return response.text(); 38 | }).then(function (source) { 39 | return { 40 | format: "module", 41 | source, 42 | shortCircuit: true 43 | }; 44 | }); 45 | } 46 | return next_load(url, context); 47 | } 48 | 49 | export {resolve, load}; 50 | -------------------------------------------------------------------------------- /webl/webl_relay.js: -------------------------------------------------------------------------------- 1 | // This Web Worker maintains a WebSocket connection between the client and the 2 | // server, attempting to reconnect indefinitely if it is severed. 3 | 4 | // The reason we maintain the connection via a Web Worker is that workers have 5 | // their own processing loop. This means that reconnection may occur even if the 6 | // window's processing loop is blocked, as it often is by a breakpoint in a 7 | // padawan. 8 | 9 | // If this worker receives a string message, it is taken to be a WebSocket URL 10 | // to connect to. Any other message is JSON encoded and sent to the server. 11 | 12 | // When the status of the connection changes, this worker sends a boolean value. 13 | // Any other value is a message from the server. 14 | 15 | /*jslint browser */ 16 | 17 | let socket; 18 | 19 | function connect_to_server(url) { 20 | socket = new WebSocket(url); 21 | socket.onopen = function () { 22 | 23 | // Inform the master that the connection is open. 24 | 25 | postMessage(true); 26 | }; 27 | socket.onclose = function () { 28 | 29 | // Inform the master that the connection is closed. Then attempt to restore it. 30 | 31 | postMessage(false); 32 | return setTimeout(connect_to_server, 250, url); 33 | }; 34 | socket.onmessage = function (event) { 35 | postMessage(JSON.parse(event.data)); 36 | }; 37 | } 38 | 39 | addEventListener("message", function (event) { 40 | if (typeof event.data === "string") { 41 | 42 | // The initial event contains the address of the WebSocket server. 43 | 44 | return connect_to_server(event.data); 45 | } 46 | return socket.send(JSON.stringify(event.data)); 47 | }); 48 | -------------------------------------------------------------------------------- /node_repl.js: -------------------------------------------------------------------------------- 1 | // This REPL evaluates JavaScript in a Node.js process. 2 | 3 | /*jslint node */ 4 | 5 | import url from "node:url"; 6 | import make_cmdl_repl from "./cmdl_repl.js"; 7 | import fileify from "./fileify.js"; 8 | const loader_url = new URL("./node_loader.js", import.meta.url); 9 | const padawan_url = new URL("./node_padawan.js", import.meta.url); 10 | 11 | function make_node_repl(capabilities, which, args = [], env = {}) { 12 | return make_cmdl_repl( 13 | capabilities, 14 | function make_command(tcp_host) { 15 | 16 | // Make sure we have predownloaded the loader and padawan scripts. By default, 17 | // Node.js is not capable of importing modules over HTTP. We specify a file 18 | // extension to force Node.js to interpret the source as a module. 19 | 20 | return Promise.all([ 21 | fileify(loader_url, ".mjs"), 22 | fileify(padawan_url, ".mjs") 23 | ]).then(function ([ 24 | loader_file_url, 25 | padawan_file_url 26 | ]) { 27 | return [ 28 | which, 29 | ...args, 30 | 31 | // Imbue the padawan process with the ability to import modules over HTTP. The 32 | // loader specifier must be a fully qualified URL on Windows. 33 | 34 | "--experimental-loader", 35 | loader_file_url.href, 36 | 37 | // Suppress the "experimental feature" warnings. 38 | 39 | "--no-warnings", 40 | 41 | // The program entry point must be specified as a path. 42 | 43 | url.fileURLToPath(padawan_file_url), 44 | tcp_host 45 | ]; 46 | }); 47 | }, 48 | env 49 | ); 50 | } 51 | 52 | export default Object.freeze(make_node_repl); 53 | -------------------------------------------------------------------------------- /deno_repl.js: -------------------------------------------------------------------------------- 1 | // This REPL evaluates JavaScript in a Deno process. 2 | 3 | // If you provide environment variables via 'env', don't forget to include 4 | // "--allow-env" in the 'args' array. 5 | 6 | /*jslint node */ 7 | 8 | import make_cmdl_repl from "./cmdl_repl.js"; 9 | const padawan_url = new URL("./deno_padawan.js", import.meta.url); 10 | 11 | function allow_host(run_args, host, permission) { 12 | 13 | // Deno only permits the --allow-net argument to appear once in its list of run 14 | // arguments. This means we need to jump thru hoops to avoid any duplication. 15 | 16 | if (run_args.includes("--allow-all") || run_args.includes(permission)) { 17 | 18 | // All hosts are already allowed. 19 | 20 | return run_args; 21 | } 22 | 23 | // If the specific form of --allow-net is present, we append 'host' onto its 24 | // list of hosts. 25 | 26 | run_args = run_args.map(function (arg) { 27 | return ( 28 | arg.startsWith(permission + "=") 29 | ? arg + "," + host 30 | : arg 31 | ); 32 | }); 33 | 34 | // Otherwise we add the --allow-net. 35 | 36 | return ( 37 | !run_args.some((arg) => arg.startsWith(permission + "=")) 38 | ? run_args.concat(permission + "=" + host) 39 | : run_args 40 | ); 41 | } 42 | 43 | function make_deno_repl(capabilities, which, args = [], env = {}) { 44 | if (padawan_url.protocol !== "file:") { 45 | args = allow_host(args, padawan_url.host, "--allow-import"); 46 | } 47 | return make_cmdl_repl( 48 | capabilities, 49 | function make_command(tcp_host, http_host) { 50 | let run_args = args; 51 | run_args = allow_host(run_args, tcp_host, "--allow-net"); 52 | run_args = allow_host(run_args, http_host, "--allow-import"); 53 | return Promise.resolve([ 54 | which, 55 | "run", 56 | ...run_args, 57 | padawan_url.href, 58 | tcp_host 59 | ]); 60 | }, 61 | Object.assign({NO_COLOR: "1"}, env) 62 | ); 63 | } 64 | 65 | export default Object.freeze(make_deno_repl); 66 | -------------------------------------------------------------------------------- /plugins/vscode/README.md: -------------------------------------------------------------------------------- 1 | # Replete VSCode extension 2 | 3 | This is a VSCode extension for [Replete](https://repletejs.org), a multi-platform JavaScript REPL. 4 | 5 | ![](https://james.diacono.com.au/talks/feedback_and_the_repl/replete.gif) 6 | 7 | [Watch the introduction](https://www.youtube.com/playlist?list=PLbPVisN8OkPDx78v-QsKdq7QL4HBVDsYX). 8 | 9 | The source code for this extension is in the Public Domain. 10 | 11 | ## Installation 12 | 13 | Make sure you have [Deno](https://deno.com) installed. In VSCode, go to 14 | 15 | View -> Extensions 16 | 17 | and search for "Replete". Press "Install". 18 | 19 | Alternatively, [download it](https://marketplace.visualstudio.com/items?itemName=jamesdiacono.Replete) from the Visual Studio Marketplace. 20 | 21 | ## Usage 22 | 23 | The following keybindings can be used to control Replete: 24 | 25 | Windows/Linux | MacOS | Command 26 | ----------------|-----------|----------- 27 | alt+r | ctrl+r | Start Replete 28 | alt+s | ctrl+s | Stop Replete 29 | alt+l | ctrl+l | Clear output 30 | alt+b | ctrl+b | Evaluate selection in browser 31 | alt+n | ctrl+n | Evaluate selection in Node.js 32 | alt+d | ctrl+d | Evaluate selection in Deno 33 | alt+u | ctrl+u | Evaluate selection in Bun 34 | alt+t | ctrl+t | Evaluate selection in Txiki 35 | 36 | Alternatively, search for "Replete" in the command palette to see the available commands. 37 | 38 | Upon starting Replete, a new Output panel will appear. This is where Replete writes its output. Now you can begin evaluating source code. 39 | 40 | ## Configuration 41 | 42 | Ensure that the Output panel's "Auto Scrolling" feature is turned on. You may also want to disable VSCode's "Smart Scroll" feature, which can interfere with Replete's output: 43 | 44 | Settings -> User -> Features -> Output 45 | 46 | The extension can be configured globally by navigating to 47 | 48 | Settings -> User -> Extensions -> Replete 49 | 50 | or by choosing "Open User Settings (JSON)" in the command palette. 51 | 52 | More configuration options are described [here](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#configuration). 53 | -------------------------------------------------------------------------------- /plugins/sublime/README.md: -------------------------------------------------------------------------------- 1 | # Replete Sublime plugin 2 | 3 | This is a Sublime Text 4 package for [Replete](https://repletejs.org), a multi-platform JavaScript REPL. 4 | 5 | The source code for this package is in the Public Domain. 6 | 7 | ## Installation 8 | 9 | Install [Deno](https://deno.com) then move the directory containing this file (README.md) into Sublime's "Packages" directory, which may need to be created: 10 | 11 | OS | Package directory 12 | --------|------------------ 13 | Linux | `~/.config/sublime-text/Packages/Replete` 14 | macOS | `~/Library/Application Support/Sublime Text/Packages/Replete` 15 | Windows | `%AppData%\Sublime Text\Packages\Replete` 16 | 17 | You may need to provide explicit paths for the directories holding your runtime binaries, such as `deno`. To do so, go to 18 | 19 | Preferences -> Package Settings -> Replete -> Settings 20 | 21 | and paste this JSON into the right-hand pane, modifying the paths as appropriate. 22 | 23 | { 24 | "env": { 25 | "PATH": "/path/to/node/bin:/path/to/deno/bin:/path/to..." 26 | } 27 | } 28 | 29 | You can also copy over and modify the "command" array, although the use of _replete.json_ files is recommended instead. 30 | 31 | ## Usage 32 | 33 | First, you must start Replete. A new tab entitled \[Replete\] is created. This is where Replete's output will appear. Now you can evaluate JavaScript. 34 | 35 | You can run commands by opening the command palette and typing "Replete". Additionally, the following keyboard shortcuts are available: 36 | 37 | Shortcut | Command 38 | --------------------|------------------- 39 | ctrl+alt+shift+r | Start Replete 40 | ctrl+alt+shift+b | Evaluate the selected region in the browser 41 | ctrl+alt+shift+n | Evaluate the selected region in Node.js 42 | ctrl+alt+shift+d | Evaluate the selected region in Deno 43 | ctrl+alt+shift+u | Evaluate the selected region in Bun 44 | ctrl+alt+shift+t | Evaluate the selected region in Txiki 45 | ctrl+alt+shift+l | Clear output 46 | 47 | To modify the keybindings, go to 48 | 49 | Preferences -> Package Settings -> Replete -> Key Bindings 50 | 51 | More configuration options are described [here](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#configuration). 52 | -------------------------------------------------------------------------------- /plugins/nrepl/README.md: -------------------------------------------------------------------------------- 1 | # Replete nREPL server 2 | 3 | This is an nREPL server for Replete (https://repletejs.org), a multi-platform JavaScript REPL. 4 | 5 | The source code is in the Public Domain. 6 | 7 | # Bugs 8 | 9 | nREPL integration is currently buggy, due to some limitations in most nREPL clients including CIDER. Output and errors that are not a direct result of evaluation may not appear in the output buffer. 10 | 11 | - https://docs.cider.mx/cider/platforms/overview.html 12 | - https://github.com/clojure-emacs/cider/discussions/3422 13 | 14 | # Usage (server) 15 | 16 | Install [Deno](https://deno.com) then run 17 | 18 | deno run --allow-all https://deno.land/x/replete/plugins/nrepl/server.js [port] 19 | 20 | from the root directory of your project. If no port number is specified, an unused port will be chosen at random. You will see a message like this written to stdout: 21 | 22 | nREPL server started on port 7888 on host 127.0.0.1 - nrepl://127.0.0.1:7888 23 | 24 | The nREPL server will spawn Replete using the command from the _replete.json_ file in the current directory, if it is present. 25 | 26 | It listens on an unused TCP port and starts a Replete process (preferring to use the command from _replete.json_) then relays messages translating between the Replete and nREPL protocols as necessary. 27 | 28 | On startup it writes the TCP port number to the _.nrepl-port_ file in the current directory. 29 | 30 | # Usage (client) 31 | 32 | The most compatible nREPL client appears to be [CIDER](https://cider.mx/), but it assumes a Clojure VM and tries to evaluate Clojure expressions at the beginning of a session. You may need to update your `~/.emacs` file with the following: 33 | 34 | (custom-set-variables 35 | '(package-selected-packages '(cider)) 36 | '(cider-repl-init-code "")) 37 | 38 | To connect, do the following: 39 | 40 | $ M-x cider-connect 41 | > Host: 42 | > Port for localhost: (type port) 43 | > Connected! ... 44 | 45 | A buffer containing a REPL will appear, be sure to check it for errors whenever evaluation appears to fail. Now switch to your JavaScript buffer: 46 | 47 | $ M-x cider-mode 48 | > Cider mode enabled in current buffer 49 | ...select some JavaScript... 50 | $ M-x cider-eval-region 51 | > => ... 52 | -------------------------------------------------------------------------------- /plugins/neovim/README.md: -------------------------------------------------------------------------------- 1 | # Replete NeoVim plugin 2 | 3 | This is a Neovim plugin for [Replete](https://repletejs.org), a multi-platform JavaScript REPL. 4 | 5 | The code for this plugin is [MIT licenced](https://opensource.org/licenses/MIT), as is its dependency [json.lua](https://github.com/rxi/json.lua). 6 | 7 | ## Installation 8 | 9 | Install [Deno](https://deno.com), ensuring `deno` is in your `PATH`, then move the directory containing this file (README.md) into Neovim's autostart directory (which may need to be created). 10 | 11 | OS | Autostart directory 12 | --------|--------------------- 13 | Linux | `~/.local/share/nvim/site/pack/plugins/start/` 14 | macOS | `~/.local/share/nvim/site/pack/plugins/start/` 15 | Windows | `%LocalAppData%\share\nvim\site\pack\plugins\start\` 16 | 17 | Restart nvim. 18 | 19 | ## Usage 20 | 21 | To start Replete, run `:Replete`. A new buffer will appear to hold Replete's output. Now you can evaluate JavaScript. The following keymaps are available: 22 | 23 | (alt+b) Evaluate the visual selection in the browser. 24 | (alt+n) Evaluate the visual selection in Node.js. 25 | (alt+d) Evaluate the visual selection in Deno. 26 | (alt+u) Evaluate the visual selection in Bun. 27 | (alt+t) Evaluate the visual selection in Txiki. 28 | 29 | To stop Replete, run `:Replete` again. 30 | 31 | By default, Replete will not let you import modules located outside of the directory in which nvim was started. 32 | 33 | ## Configuration 34 | 35 | Configuration options are described [here](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#configuration). 36 | 37 | Although per-project configuration is recommended, the plugin may also be configured by pasting the following code into Neovim's configuration file, located at 38 | 39 | OS | Config file 40 | --------|------------------ 41 | Linux | `~/.config/nvim/init.vim` 42 | macOS | `~/.config/nvim/init.vim` 43 | Windows | `%LocalAppData%\nvim\init.vim` 44 | 45 | and adjusting the values as necessary: 46 | 47 | let g:replete_command = [ 48 | \"/path/to/deno", 49 | \"run", 50 | \"--allow-all", 51 | ... 52 | \] 53 | let g:replete_cwd = "/path/to/your/source/code/directory" 54 | 55 | If `replete_cwd` is specified, ensure it points to a directory above every module that might be imported, directly or indirectly, during evaluation. 56 | 57 | Restart nvim for the changes to take effect. 58 | -------------------------------------------------------------------------------- /tjs_padawan.js: -------------------------------------------------------------------------------- 1 | // The padawan program for a Txiki CMDL. See cmdl.js. 2 | 3 | // $ tjs run /path/to/tjs_padawan.js : 4 | 5 | /*jslint tjs, global, null */ 6 | 7 | function reason(exception) { 8 | try { 9 | if (exception?.stack !== undefined) { 10 | return ( 11 | exception.name + ": " + exception.message 12 | + "\n" + exception.stack 13 | ); 14 | } 15 | return "Exception: " + tjs.inspect(exception); 16 | } catch (_) { 17 | return "Exception"; 18 | } 19 | } 20 | 21 | function evaluate(script, import_specifiers, wait) { 22 | return Promise.all( 23 | import_specifiers.map(function (specifier) { 24 | return import(specifier); 25 | }) 26 | ).then(function (modules) { 27 | globalThis.$imports = modules; 28 | const value = globalThis.eval(script); 29 | return ( 30 | wait 31 | ? Promise.resolve(value).then(tjs.inspect) 32 | : tjs.inspect(value) 33 | ); 34 | }).then(function (evaluation) { 35 | return {evaluation}; 36 | }).catch(function (exception) { 37 | return {exception: reason(exception)}; 38 | }); 39 | } 40 | 41 | // The following code is copied from deno_padawan.js. 42 | 43 | let connection; 44 | let buffer = new Uint8Array(0); 45 | 46 | function consume() { 47 | const string = new TextDecoder().decode(buffer); 48 | const parts = string.split("\n"); 49 | if (parts.length === 1) { 50 | return; 51 | } 52 | const command = JSON.parse(parts[0]); 53 | evaluate( 54 | command.script, 55 | command.imports, 56 | command.wait 57 | ).then(function (report) { 58 | report.id = command.id; 59 | return connection.write(new TextEncoder().encode( 60 | JSON.stringify(report) + "\n" 61 | )); 62 | }); 63 | buffer = new TextEncoder().encode( 64 | parts.slice(1).join("\n") 65 | ); 66 | return consume(); 67 | } 68 | 69 | function receive(bytes) { 70 | const concatenated = new Uint8Array(buffer.length + bytes.length); 71 | concatenated.set(buffer); 72 | concatenated.set(bytes, buffer.length); 73 | buffer = concatenated; 74 | } 75 | 76 | let chunk = new Uint8Array(16640); 77 | 78 | function read() { 79 | return connection.read(chunk).then(function (nr_bytes) { 80 | if (nr_bytes === null) { 81 | throw new Error("Connection closed."); 82 | } 83 | receive(chunk.slice(0, nr_bytes)); 84 | consume(); 85 | return read(); 86 | }); 87 | } 88 | 89 | // Connect to the TCP server on the specified port, and wait for instructions. 90 | 91 | const [hostname, port_string] = tjs.args.slice().pop().split(":"); 92 | const port = parseInt(port_string); 93 | tjs.connect("tcp", hostname, port).then(function (the_connection) { 94 | connection = the_connection; 95 | return read(); 96 | }); 97 | -------------------------------------------------------------------------------- /plugins/mcp/README.md: -------------------------------------------------------------------------------- 1 | # Replete MCP server 2 | 3 | A [Model Context Protocol](https://modelcontextprotocol.io/) server for [Replete](https://repletejs.org). It provides LLM agents with tools to evaluate JavaScript in a variety of platforms including Deno, Node.js, and the browser. 4 | 5 | The source code for this server is in the Public Domain. 6 | 7 | ## Warning 8 | 9 | Code evaluated in any platform other than the browser (such as Deno) has uninhibited access to the filesystem, network, etc. Allowing your agent to evaluate code using Replete is equivalent to giving it access to your terminal. To ensure agent activity is properly sandboxed, Replete can be configured with only the browser REPL by passing [`--which_deno=""`](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#optionswhich_deno---which_deno) (assuming Replete is hosted by Deno, as it is by default). 10 | 11 | ## Installation 12 | 13 | Install [Deno](https://deno.com) then configure your editor to start the MCP server using this command: 14 | 15 | deno run --allow-all https://deno.land/x/replete/plugins/mcp/server.js 16 | 17 | MCP messages are read from stdin and written to stdout. 18 | 19 | To configure the Cursor editor, for example, go to Settings -> Cursor Settings -> MCP & Integrations -> New MCP Server and paste the following: 20 | 21 | { 22 | "mcpServers": { 23 | "replete": { 24 | "command": "deno", 25 | "args": [ 26 | "run", 27 | "--allow-all", 28 | "https://deno.land/x/replete/plugins/mcp/server.js" 29 | ] 30 | } 31 | } 32 | } 33 | 34 | If you have problems, try running the server with the 35 | [MCP inspector](https://modelcontextprotocol.io/legacy/tools/inspector). 36 | 37 | ## Usage 38 | 39 | Tools provided include _restart_, _stop_, _evaluate_, and _output_. 40 | 41 | The _restart_ tool starts Replete, or restarts it if it is already running. It takes a `cwd` parameter, which is the absolute path to your project's root directory. This is where the server will look for a _replete.json_ file (see below). 42 | 43 | The _stop_ tool stops Replete. 44 | 45 | The _evaluate_ tool evaluates code and reports the result, providing Replete is running. It takes the following parameters: 46 | 47 | - `source`: the source code to be evaluated (required) 48 | - `platform`: one of `"browser"`, `"deno"`,`"node"`, etc. (required) 49 | - `locator`: the `file://` URL of the file containing the source (required only if the source contains relative imports) 50 | 51 | The _output_ tool reports any logs and errors that have occurred since the last call to _output_ or _evaluate_. When evaluated code is expected to run over many turns, this tool must be called to discover the result. 52 | 53 | When evaluating code in the browser, you may want your agent to observe and interact with the page. This can be accomplished by installing the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) or similar and using it to navigate to and interact with the WEBL. 54 | 55 | Configuration of Replete is described [here](https://github.com/jamesdiacono/Replete?tab=readme-ov-file#configuration). 56 | -------------------------------------------------------------------------------- /webl/README.md: -------------------------------------------------------------------------------- 1 | # The WEBL 2 | 3 | > "The WEBL alliance is too well equipped." 4 | > -- Admiral Motti 5 | 6 | The WEBL (expansion forthcoming) provides a means of evaluating JavaScript source code in isolated execution contexts, or __padawans__, in the browser. See webl.js for usage instructions. 7 | 8 | The WEBL is in the Public Domain. 9 | 10 | # The WEBL server 11 | 12 | It is possible to control one or more WEBLs from a Node.js process. WEBLs can even be run in different browsers simultaneously, for example in Firefox and Safari. 13 | 14 | The below diagram illustrates the communication between the various components. See webl_server.js for usage instructions. 15 | 16 | +---------------+ 17 | | | 18 | | WEBL server | 19 | | | 20 | +-------+-------+ 21 | | Node.js process 22 | | 23 | - - - - -|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 | | 25 | | Browser tabs 26 | | 27 | +------------------------------------------------------+ 28 | | | 29 | | | 30 | +---------+----------------------------------------+ +--------v--------+ 31 | | | | | | 32 | | | WEBL client | | WEBL client | 33 | | | | | | 34 | | +-------v------+ | | ... | 35 | | | | | | | 36 | | | WEBL relay +--------+ | +-----------------+ 37 | | | (worker) | | | 38 | | | | | | +-----------+ 39 | | +--------------+ +-----v----+ | | | 40 | | | | | | Padawan | 41 | | | WEBL +-------------------+-----+-----> (popup) | 42 | | | | | | | | 43 | | +-----+----+ | | +-----------+ 44 | | | | | 45 | | +-----------+-----+-----+-----------+ | | +-----------+ 46 | | | | | | | | | | 47 | | | | | | | +-----> Padawan | 48 | | +----v---+ +----v---+ +----v---+ +----v---+ | | (popup) | 49 | | |Padawan | |Padawan | |Padawan | |Padawan | | | | 50 | | | (top) | |(iframe)| |(worker)| |(worker)| | +-----------+ 51 | | +--------+ +--------+ +--------+ +--------+ | 52 | | | 53 | +--------------------------------------------------+ 54 | -------------------------------------------------------------------------------- /deno_padawan.js: -------------------------------------------------------------------------------- 1 | // The padawan program for a Deno CMDL. See cmdl.js. 2 | 3 | // $ deno run /path/to/deno_padawan.js : 4 | 5 | /*jslint deno, global, null */ 6 | 7 | function evaluate(script, import_specifiers, wait) { 8 | 9 | // The 'evaluate' function evaluates the 'script', after resolving any imported 10 | // modules. It returns a Promise that resolves to a report object. 11 | 12 | return Promise.all( 13 | import_specifiers.map(function (specifier) { 14 | return import(specifier); 15 | }) 16 | ).then(function (modules) { 17 | 18 | // The imported modules are provided as a global variable. 19 | 20 | globalThis.$imports = modules; 21 | 22 | // The script is evaluated using an "indirect" eval, depriving it of access to 23 | // the local scope. 24 | 25 | const value = globalThis.eval(script); 26 | return ( 27 | wait 28 | ? Promise.resolve(value).then(Deno.inspect) 29 | : Deno.inspect(value) 30 | ); 31 | }).then(function (evaluation) { 32 | return {evaluation}; 33 | }).catch(function (exception) { 34 | return { 35 | exception: ( 36 | typeof exception?.stack === "string" 37 | ? exception.stack 38 | : "Exception: " + Deno.inspect(exception) 39 | ) 40 | }; 41 | }); 42 | } 43 | 44 | let connection; 45 | let buffer = new Uint8Array(0); 46 | 47 | function consume() { 48 | 49 | // The 'consume' function runs every command in the buffer in parallel. 50 | 51 | const string = new TextDecoder().decode(buffer); 52 | const parts = string.split("\n"); 53 | if (parts.length === 1) { 54 | 55 | // There is not yet a complete command in the buffer. Wait for more bytes. 56 | 57 | return; 58 | } 59 | const command = JSON.parse(parts[0]); 60 | 61 | // Evaluate the script, eventually sending a report back to the server. 62 | 63 | evaluate( 64 | command.script, 65 | command.imports, 66 | command.wait 67 | ).then(function (report) { 68 | report.id = command.id; 69 | return connection.write(new TextEncoder().encode( 70 | JSON.stringify(report) + "\n" 71 | )); 72 | }); 73 | 74 | // Immediately run any remaining commands in the buffer. 75 | 76 | buffer = new TextEncoder().encode( 77 | parts.slice(1).join("\n") 78 | ); 79 | return consume(); 80 | } 81 | 82 | function receive(bytes) { 83 | 84 | // Appends an array of bytes to the buffer. 85 | 86 | const concatenated = new Uint8Array(buffer.length + bytes.length); 87 | concatenated.set(buffer); 88 | concatenated.set(bytes, buffer.length); 89 | buffer = concatenated; 90 | } 91 | 92 | let chunk = new Uint8Array(16640); 93 | 94 | function read() { 95 | 96 | // Read the incoming bytes into the buffer. 97 | 98 | return connection.read(chunk).then(function (nr_bytes) { 99 | if (nr_bytes === null) { 100 | throw new Error("Connection closed."); 101 | } 102 | receive(chunk.slice(0, nr_bytes)); 103 | consume(); 104 | return read(); 105 | }); 106 | } 107 | 108 | // Connect to the TCP server and wait for instructions. 109 | 110 | const [hostname, port_string] = Deno.args[0].split(":"); 111 | const port = parseInt(port_string); 112 | Deno.connect({hostname, port}).then(function (the_connection) { 113 | connection = the_connection; 114 | addEventListener("unhandledrejection", function (event) { 115 | event.preventDefault(); 116 | globalThis.console.error(event.reason); 117 | }); 118 | addEventListener("error", function (event) { 119 | event.preventDefault(); 120 | globalThis.console.error(event.error); 121 | }); 122 | return read(); 123 | }); 124 | -------------------------------------------------------------------------------- /cmdl_repl.js: -------------------------------------------------------------------------------- 1 | // A generic REPL for command-line runtimes. It uses a CMDL and serves modules 2 | // from a dedicated HTTP server. 3 | 4 | /*jslint node */ 5 | 6 | import http from "node:http"; 7 | import make_cmdl from "./cmdl.js"; 8 | import make_repl from "./repl.js"; 9 | 10 | // This should be "localhost", but we force IPv4 because, on Windows, Node.js 11 | // seems unwilling to connect to Deno over IPv6. 12 | 13 | const http_server_hostname = "127.0.0.1"; 14 | 15 | function make_cmdl_repl(capabilities, make_command, env) { 16 | let repl; 17 | 18 | // An HTTP server serves modules to the padawan, which imports them via the 19 | // dynamic 'import' function. As such, the padawan is expected to support HTTP 20 | // imports. 21 | 22 | let http_server; 23 | let http_server_port; 24 | 25 | const cmdl = make_cmdl( 26 | function spawn_padawan(tcp_host) { 27 | const http_host = http_server_hostname + ":" + http_server_port; 28 | return make_command(tcp_host, http_host).then(function (command) { 29 | return capabilities.spawn(command, env, [tcp_host, http_host]); 30 | }); 31 | }, 32 | function on_stdout(buffer) { 33 | return capabilities.out(buffer.toString()); 34 | }, 35 | function on_stderr(buffer) { 36 | return capabilities.err(buffer.toString()); 37 | } 38 | ); 39 | 40 | function on_start() { 41 | http_server = http.createServer(function (req, res) { 42 | return repl.serve( 43 | new URL(req.url, "http://" + req.host).href, 44 | req.headers 45 | ).then(function ({body, headers}) { 46 | Object.entries(headers).forEach(function ([key, value]) { 47 | res.setHeader(key, value); 48 | }); 49 | res.end(body); 50 | }).catch(function fail(reason) { 51 | capabilities.err(reason.stack + "\n"); 52 | res.statusCode = 500; 53 | return res.end(); 54 | }); 55 | }); 56 | return Promise.all([ 57 | new Promise(function start_http_server(resolve, reject) { 58 | http_server.on("error", reject); 59 | return http_server.listen(0, http_server_hostname, function () { 60 | http_server_port = http_server.address().port; 61 | return resolve(); 62 | }); 63 | }), 64 | cmdl.create() 65 | ]); 66 | } 67 | 68 | function on_eval( 69 | on_result, 70 | produce_script, 71 | dynamic_specifiers, 72 | import_specifiers, 73 | wait 74 | ) { 75 | return cmdl.eval( 76 | produce_script(dynamic_specifiers), 77 | import_specifiers, 78 | wait 79 | ).then(function (report) { 80 | return on_result(report.evaluation, report.exception); 81 | }); 82 | } 83 | 84 | function on_stop() { 85 | return Promise.all([ 86 | new Promise(function (resolve) { 87 | return http_server.close(resolve); 88 | }), 89 | cmdl.destroy() 90 | ]); 91 | } 92 | 93 | function specify(locator) { 94 | return ( 95 | locator.startsWith("file:///") 96 | ? ( 97 | "http://" + http_server_hostname + ":" + http_server_port 98 | + locator.replace("file://", "") 99 | ) 100 | : locator 101 | ); 102 | } 103 | 104 | repl = make_repl( 105 | capabilities, 106 | on_start, 107 | on_eval, 108 | on_stop, 109 | specify 110 | ); 111 | return repl; 112 | } 113 | 114 | export default Object.freeze(make_cmdl_repl); 115 | -------------------------------------------------------------------------------- /webl/r2d2.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 46 | 47 | 48 | 51 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /fileify.js: -------------------------------------------------------------------------------- 1 | // Stores a remote file locally, for offline use. 2 | 3 | /*jslint node */ 4 | 5 | import console from "node:console"; 6 | import crypto from "node:crypto"; 7 | import fs from "node:fs"; 8 | import os from "node:os"; 9 | import path from "node:path"; 10 | import process from "node:process"; 11 | import url from "node:url"; 12 | 13 | function user_cache_dir() { 14 | 15 | // Returns the path to the user's cache directory, a more permanent alternative 16 | // to the system's temporary directory which gets cleaned out every few days. 17 | 18 | // Platform | Path | Example 19 | // ---------|---------------------------------|--------------------------------- 20 | // Linux | $XDG_CACHE_HOME or $HOME/.cache | /home/me/.cache 21 | // macOS | $HOME/Library/Caches | /Users/me/Library/Caches 22 | // Windows | $LOCALAPPDATA | C:\Users\me\AppData\Local 23 | 24 | if (os.platform() === "win32") { 25 | return process.env.LOCALAPPDATA; 26 | } 27 | if (os.platform() === "darwin") { 28 | return path.join(process.env.HOME, "Library", "Caches"); 29 | } 30 | return process.env.XDG_CACHE_HOME ?? path.join(process.env.HOME, ".cache"); 31 | } 32 | 33 | function replete_cache_dir() { 34 | try { 35 | return path.join(user_cache_dir(), "replete"); 36 | } catch (_) { 37 | return os.tmpdir(); 38 | } 39 | } 40 | 41 | function fileify(http_url, replace_extension) { 42 | 43 | // If the URL is already a file URL, we are done. 44 | 45 | if (http_url.protocol === "file:") { 46 | return Promise.resolve(http_url); 47 | } 48 | 49 | function versioned_path(vary) { 50 | 51 | // Construct a temporary path for the file, based on the HTTP URL. 52 | 53 | const extension = path.extname(http_url.pathname); 54 | const name = path.basename(http_url.pathname, extension); 55 | const version = crypto.createHash( 56 | "md5" 57 | ).update( 58 | vary 59 | ).digest( 60 | "hex" 61 | ).slice(0, 8); 62 | return path.join( 63 | replete_cache_dir(), 64 | name + "." + version + (replace_extension ?? extension) 65 | ); 66 | } 67 | 68 | // Check if a cached version of the file is available. 69 | 70 | let file = versioned_path(http_url.href); 71 | return fs.promises.stat(file).catch(function () { 72 | 73 | // The file is not cached, so download it to the filesystem. 74 | 75 | return fetch(http_url).then(function (response) { 76 | if (!response.ok) { 77 | return Promise.reject( 78 | new Error("Failed to download '" + http_url.href + "'.") 79 | ); 80 | } 81 | 82 | // Should the file be cached indefinitely? Only if the Cache-Control header 83 | // indicates that the file is immutable. 84 | 85 | const immutable = ( 86 | response.headers.has("cache-control") 87 | && response.headers.get("cache-control").includes("immutable") 88 | ); 89 | if (!immutable) { 90 | file = versioned_path(crypto.randomUUID()); 91 | } 92 | return response.arrayBuffer(); 93 | }).then(function ensure_directory(array_buffer) { 94 | return fs.promises.mkdir( 95 | path.dirname(file), 96 | {recursive: true, mode: 0o700} 97 | ).then(function create_file() { 98 | return fs.promises.writeFile( 99 | file, 100 | new Uint8Array(array_buffer), 101 | {mode: 0o700} 102 | ); 103 | }); 104 | }); 105 | }).then(function () { 106 | return url.pathToFileURL(file); 107 | }); 108 | } 109 | 110 | if (import.meta.main) { 111 | fileify( 112 | new URL("https://deno.land/x/replete/node_loader.js"), 113 | ".mjs" 114 | ).then(console.log); 115 | } 116 | 117 | export default Object.freeze(fileify); 118 | -------------------------------------------------------------------------------- /node_padawan.js: -------------------------------------------------------------------------------- 1 | // The padawan program for the Node.js and Bun CMDLs. See cmdl.js. 2 | 3 | // $ node /path/to/node_padawan.js : 4 | // $ bun run /path/to/node_padawan.js : 5 | 6 | // Exceptions that occur outside of evaluation are printed to stderr. 7 | 8 | /*jslint node, bun, global */ 9 | 10 | import console from "node:console"; 11 | import process from "node:process"; 12 | import net from "node:net"; 13 | import util from "node:util"; 14 | import readline from "node:readline"; 15 | 16 | // Bun does not yet support HTTP imports. Pending 17 | // https://github.com/oven-sh/bun/issues/38, we use a plugin to polyfill this 18 | // behavior. 19 | 20 | const rx_any = /./; 21 | const rx_http = /^https?:\/\//; 22 | const rx_path = /^\.*\//; 23 | 24 | function load_http_module(href) { 25 | return fetch(href).then(function (response) { 26 | return response.text().then(function (text) { 27 | return ( 28 | response.ok 29 | ? {contents: text, loader: "js"} 30 | : Promise.reject( 31 | new Error("Failed to load module '" + href + "': " + text) 32 | ) 33 | ); 34 | }); 35 | }); 36 | } 37 | 38 | if (typeof Bun === "object") { 39 | Bun.plugin({ 40 | name: "http_imports", 41 | setup(build) { 42 | build.onResolve({filter: rx_path}, function (args) { 43 | if (rx_http.test(args.importer)) { 44 | return {path: new URL(args.path, args.importer).href}; 45 | } 46 | }); 47 | build.onLoad({filter: rx_any, namespace: "http"}, function (args) { 48 | return load_http_module("http:" + args.path); 49 | }); 50 | build.onLoad({filter: rx_any, namespace: "https"}, function (args) { 51 | return load_http_module("https:" + args.path); 52 | }); 53 | } 54 | }); 55 | } 56 | 57 | function evaluate(script, import_specifiers, wait) { 58 | return Promise.all( 59 | import_specifiers.map(function (specifier) { 60 | return import(specifier); 61 | }) 62 | ).then(function (modules) { 63 | globalThis.$imports = modules; 64 | const value = globalThis.eval(script); 65 | return ( 66 | wait 67 | ? Promise.resolve(value).then(util.inspect) 68 | : util.inspect(value) 69 | ); 70 | }).then(function (evaluation) { 71 | return {evaluation}; 72 | }).catch(function (exception) { 73 | return { 74 | exception: ( 75 | typeof Bun === "object" 76 | ? Bun.inspect(exception) 77 | : ( 78 | typeof exception?.stack === "string" 79 | ? exception.stack 80 | : "Exception: " + util.inspect(exception) 81 | ) 82 | ) 83 | }; 84 | }); 85 | } 86 | 87 | // Connect to the TCP server and wait for instructions. 88 | 89 | const [hostname, port_string] = process.argv[2].split(":"); 90 | const socket = net.connect(parseInt(port_string), hostname); 91 | socket.once("connect", function () { 92 | readline.createInterface({input: socket}).on("line", function (line) { 93 | 94 | // Parse each line as a command object. Evaluate the script, eventually sending 95 | // a report back to the server. 96 | 97 | const command = JSON.parse(line); 98 | return evaluate( 99 | command.script, 100 | command.imports, 101 | command.wait 102 | ).then( 103 | function on_evaluated(report) { 104 | report.id = command.id; 105 | return socket.write(JSON.stringify(report) + "\n"); 106 | } 107 | ); 108 | }); 109 | 110 | // Uncaught exceptions that occur outside of evaluation are non-fatal. They are 111 | // caught by a global handler and written to stderr. 112 | 113 | process.on("uncaughtException", console.error); 114 | process.on("unhandledRejection", console.error); 115 | }); 116 | socket.once("error", function (error) { 117 | 118 | // Any problem with the transport mechanism results in the immediate termination 119 | // of the process. 120 | 121 | console.error(error); 122 | return process.exit(1); 123 | }); 124 | -------------------------------------------------------------------------------- /replete.js: -------------------------------------------------------------------------------- 1 | // This is the standard Replete program. 2 | 3 | // It exposes a command line interface facilitating basic configuration. If you 4 | // need more control over Replete, use ./run.js directly. 5 | 6 | // This program can be run from the command line using any runtime that 7 | // implements the Node.js built-in modules, such as "node:fs". The choice of 8 | // runtime used to run Replete does not affect which REPLs are available, 9 | // because each REPL is run as a separate process. 10 | 11 | // To start Replete in Node.js v19.0.0+, run 12 | 13 | // $ node /path/to/replete.js [options] 14 | 15 | // To start Replete in Deno v1.35.3+, run 16 | 17 | // $ deno run --allow-all /path/to/replete.js [options] 18 | 19 | // or, skipping installation entirely, 20 | 21 | // $ deno run \ 22 | // --allow-all \ 23 | // --importmap https://deno.land/x/replete/import_map.json \ 24 | // https://deno.land/x/replete/replete.js \ 25 | // [options] 26 | 27 | // To start Replete in Bun v1.1.0+, run 28 | 29 | // $ bun run /path/to/replete.js [options] 30 | 31 | // The following options are supported: 32 | 33 | // --content_type=: 34 | // See README.md. 35 | 36 | // --browser_port= 37 | // See README.md. 38 | 39 | // --browser_hostname= 40 | // See README.md. 41 | 42 | // --which_node= 43 | // See README.md. 44 | 45 | // --node_debugger_port= 46 | // A Node.js debugger will attempt to listen on the specified port. 47 | // This makes it possible to monitor your evaluations using a fully 48 | // featured debugger. To attach a debugger, open Google Chrome and 49 | // navigate to chrome://inspect. 50 | 51 | // --which_deno= 52 | // See README.md. 53 | 54 | // --deno_debugger_port= 55 | // Like the --node_debugger_port option, but for Deno. Exposes the V8 56 | // Inspector Protocol. 57 | 58 | // --which_bun= 59 | // See README.md. 60 | 61 | // --bun_debugger_port= 62 | // Like the --node_debugger_port option, but for Bun. Exposes the 63 | // WebKit Inspector Protocol. 64 | 65 | // --which_tjs= 66 | // See README.md. 67 | 68 | // The process communicates via its stdin and stdout. See ./run.js for a 69 | // description of the stream protocol. 70 | 71 | // The REPLs will not be able to read files outside the current working 72 | // directory. 73 | 74 | /*jslint node */ 75 | 76 | import process from "node:process"; 77 | import run from "./run.js"; 78 | 79 | let content_type_object = Object.create(null); 80 | let options = { 81 | node_args: [], 82 | 83 | // The Deno REPL is run with unlimited permissions. This seems justified for 84 | // development, where it is not known in advance what the REPL may be asked to 85 | // do. 86 | 87 | deno_args: ["--allow-all", "--no-lock"], 88 | bun_args: [] 89 | }; 90 | 91 | // Parse the command line arguments into an options object. 92 | 93 | process.argv.slice(2).forEach(function (argument) { 94 | const [_, name, value] = argument.match(/^--(\w+)=(.*)$/); 95 | if (name === "content_type") { 96 | const [file_extension, type] = value.split(":"); 97 | content_type_object[file_extension] = type; 98 | } else { 99 | options[name] = ( 100 | name.endsWith("_port") 101 | ? parseInt(value) 102 | : value 103 | ); 104 | } 105 | }); 106 | if (Number.isSafeInteger(options.node_debugger_port)) { 107 | options.node_args.push("--inspect=" + options.node_debugger_port); 108 | delete options.node_debugger_port; 109 | } 110 | if (Number.isSafeInteger(options.deno_debugger_port)) { 111 | options.deno_args.push("--inspect=127.0.0.1:" + options.deno_debugger_port); 112 | delete options.deno_debugger_port; 113 | } 114 | if (Number.isSafeInteger(options.bun_debugger_port)) { 115 | options.bun_args.push("--inspect=" + options.bun_debugger_port); 116 | delete options.bun_debugger_port; 117 | } 118 | if (Object.keys(content_type_object).length > 0) { 119 | options.headers = function (locator) { 120 | const file_extension = locator.split(".").pop().toLowerCase(); 121 | const content_type = content_type_object[file_extension]; 122 | if (content_type !== undefined) { 123 | return {"Content-Type": content_type}; 124 | } 125 | }; 126 | } 127 | 128 | run(options); 129 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | // The 'run' function starts a Replete instance, attaching it to the current 2 | // process's stdin and stdout. It can only be called once per process. It 3 | // handles termination signals gracefully. It takes an 'options' object 4 | // described in ./README.md and returns an 'exit' function that safely stops 5 | // Replete and exits the process. 6 | 7 | // Messages are sent in both directions, each occupying a single line. A message 8 | // is a JSON-encoded object. Command messages are read from stdin, and result 9 | // messages are written to stdout. This is the standard interface that text 10 | // editor plugins are expected to adhere to. 11 | 12 | // For example, 13 | 14 | // STDIN {"platform": "browser", "source": "navigator.vendor"} 15 | // STDOUT {"evaluation": "Google Inc."} 16 | 17 | // See ./make.js for a description of the message protocol. 18 | 19 | // Here is an example program, custom_replete.js, that serves the WEBL on port 20 | // 3000, gives the Deno REPL full permissions, and serves CSS files in addition 21 | // to JavaScript files. 22 | 23 | // import run from "https://deno.land/x/replete/run.js"; 24 | // run({ 25 | // browser_port: 3000, 26 | // deno_args: ["--allow-all", "--no-lock"], 27 | // headers(locator) { 28 | // if (locator.endsWith(".js")) { 29 | // return {"Content-Type": "text/javascript"}; 30 | // } 31 | // if (locator.endsWith(".css")) { 32 | // return {"Content-Type": "text/css"}; 33 | // } 34 | // } 35 | // }); 36 | 37 | // It could be run from the command line like 38 | 39 | // $ deno run \ 40 | // --allow-all \ 41 | // --importmap https://deno.land/x/replete/import_map.json \ 42 | // custom_replete.js 43 | 44 | /*jslint node, deno, bun */ 45 | 46 | import os from "node:os"; 47 | import process from "node:process"; 48 | import readline from "node:readline"; 49 | import url from "node:url"; 50 | import make_replete from "./make.js"; 51 | 52 | function run(options) { 53 | 54 | function on_result(message) { 55 | process.stdout.write(JSON.stringify(message) + "\n"); 56 | } 57 | 58 | options = Object.assign({}, options); 59 | options.on_result = on_result; 60 | if (options.root_locator === undefined) { 61 | const cwd_href = url.pathToFileURL(process.cwd()).href; 62 | options.root_locator = ( 63 | cwd_href.endsWith("/") 64 | ? cwd_href 65 | : cwd_href + "/" 66 | ); 67 | } 68 | if ( 69 | typeof Deno === "object" 70 | && options.which_deno === undefined 71 | && options.which_deno !== "" 72 | ) { 73 | options.which_deno = Deno.execPath(); 74 | } else if ( 75 | typeof Bun === "object" 76 | && options.which_bun === undefined 77 | && options.which_bun !== "" 78 | ) { 79 | options.which_bun = process.argv[0]; 80 | } else if ( 81 | options.which_node === undefined 82 | && options.which_node !== "" 83 | ) { 84 | options.which_node = process.argv[0]; 85 | } 86 | if (options.node_env === undefined) { 87 | options.node_env = process.env; 88 | } 89 | if (options.deno_env === undefined) { 90 | options.deno_env = process.env; 91 | } 92 | if (options.bun_env === undefined) { 93 | options.bun_env = process.env; 94 | } 95 | if (options.tjs_env === undefined) { 96 | options.tjs_env = process.env; 97 | } 98 | const {start, send, stop} = make_replete(options); 99 | 100 | function exit() { 101 | stop().then(function () { 102 | process.exit(); 103 | }); 104 | } 105 | 106 | start().then(function () { 107 | const line_reader = readline.createInterface({input: process.stdin}); 108 | 109 | // The closure of stdin is a reliable way to detect unclean termination of the 110 | // parent process, for example via SIGKILL, allowing us to avoid zombification. 111 | 112 | line_reader.on("close", exit); 113 | line_reader.on("line", function (line) { 114 | if (line.trim() === "") { 115 | return; 116 | } 117 | let message; 118 | try { 119 | message = JSON.parse(line); 120 | } catch (exception) { 121 | return on_result({err: exception.stack + "\n"}); 122 | } 123 | send(message).catch(function (error) { 124 | on_result({ 125 | exception: error.stack, 126 | id: message.id 127 | }); 128 | }); 129 | }); 130 | }).catch(function (error) { 131 | on_result({err: error.stack + "\n"}); 132 | }); 133 | process.on("SIGTERM", exit); 134 | process.on("SIGINT", exit); 135 | if (os.platform() !== "win32") { 136 | process.on("SIGHUP", exit); 137 | } 138 | return exit; 139 | } 140 | 141 | export default Object.freeze(run); 142 | -------------------------------------------------------------------------------- /plugins/mcp/methodology.md: -------------------------------------------------------------------------------- 1 | # REPL-driven development 2 | 3 | This document describes a methodology for developing JavaScript modules using REPL-driven development. It involves using [Replete](https://repletejs.org), a multi-platform JavaScript REPL, to frequently evaluate expressions, statements, and whole files. This not only provides feedback on the behavior of code as it is written, but also lets us conveniently test modules in total isolation. 4 | 5 | Unlike most JavaScript REPLs, Replete supports the evaluation of import statements and other module syntax. This means that, in general, it is possible to evaluate the entire text of compatible JavaScript modules without error on at least one platform (Deno, Node.js, the browser, etc.). For example, user interface components can only be evaluated in the browser, whereas modules that export pure functions can be evaluated in any platform. 6 | 7 | ## Whole Modules 8 | 9 | Due to the runtime semantics of `import.meta.main`, evaluating a module can have a different effect than importing that same module. We take advantage of this duality to embed "demos" inside modules that effectively function as a unit test: these run when `import.meta.main` is `true`, and do not run when it is `false` or `undefined`. This approach is called [Whole Modules](https://james.diacono.com.au/whole_modules.html). 10 | 11 | For example: 12 | 13 | function double (number) { 14 | return 2 * number; 15 | } 16 | 17 | if (import.meta.main) { 18 | if (double(3) !== 6) { 19 | throw new Error("FAIL positive"); 20 | } 21 | if (double(-2) !== -3) { 22 | throw new Error("FAIL negative"); 23 | } 24 | } 25 | 26 | export default Object.freeze(double); 27 | 28 | For modules without a user interface aspect, demos are generally a test that produces a pass or a fail result. Failure is indicated by an exception or unhandled Promise rejection, such that if the module was run directly in Deno (`deno run my_module.js`) then the pass/fail result would be encoded in the exit code of the process. 29 | 30 | An example of a pure function with a test is [crc32.js](https://repletejs.org/play/crc32.js). 31 | 32 | ## UI 33 | 34 | User interface code must be evaluated in the browser REPL. When Replete is started, it prints the HTTP address of the WEBL (`Waiting for WEBL: http://localhost:9325`), which is essentially a blank canvas that you open in a browser tab. The WEBL's DOM can be manipulated via evaluation, for example evaluating 35 | 36 | document.body.style.background = "fuchsia"; 37 | 38 | would change the background color. 39 | 40 | For user interface components, demos generally render the component with randomized parameters as well as simulated delays and failures. Because demos must run in isolation, they can not rely on any global state (such as global CSS classes) and must import and initialize such functionality explicitly (either in the demo or the component). 41 | 42 | UI demos should begin by clearing any residual content from the DOM via a reset such as `document.documentElement.innerHTML = ""`. 43 | 44 | An example of a UI component is [split_ui.js](https://repletejs.org/play/split_ui.js). Not all UI code needs to be a component, for example the demo in [pkzip.js](https://repletejs.org/play/pkzip.js). 45 | 46 | ## Method 47 | 48 | When writing a new module, begin with an empty file. Write something concrete, for example 49 | 50 | const boolean = Math.random() < 0.5; 51 | 52 | or 53 | 54 | fetch( 55 | "http://my-api.com" 56 | ).then(function (response) { 57 | return response.json(); 58 | }).then( 59 | console.log 60 | ) 61 | 62 | or 63 | 64 | const button = document.createElement("button"); 65 | button.textContent = "Click me"; 66 | button.style.fontSize = "30px"; 67 | document.body.append(button); 68 | 69 | and then evaluate it and examine the result (in Replete's output or in the WEBL). Do _not_ write or modify more than a few lines of code between evaluations. Aim for small, safe steps, the aim being to develop a module whose parts have each been well exercised. If you have a tendency to write large amounts of code up front, you must exercise self restraint. Gradually build out the required functionality, creating abstractions (such as parameterized functions) as necessary, and eventually wrap the demo in a conditional and export the relevant interface. The resulting module should follow this pattern: 70 | 71 | IMPLEMENTATION 72 | 73 | if (import.meta.main) { 74 | DEMO 75 | } 76 | 77 | export default Object.freeze(INTERFACE); 78 | 79 | When modifying an existing module, first run its demo to get a feel for the current and expected behavior. 80 | 81 | ## Common misunderstandings 82 | 83 | To run a module's demo, for example `my_module.js`, there is no need to devise a harness script such as `import "./my_module.js";` or similar. Rather, just evaluate the source code of the entire file verbatim. 84 | 85 | In demos, there is no need to catch errors and log them. Leave exceptions and Promise rejections uncaught, thereby failing more loudly. 86 | 87 | Demos should never take command line arguments. They are evaluated in Replete, where providing external arguments is not possible. 88 | -------------------------------------------------------------------------------- /plugins/emacs/replete.el: -------------------------------------------------------------------------------- 1 | (defvar replete-default-command 2 | (list "deno" 3 | "run" 4 | "--allow-all" 5 | "--importmap" 6 | "https://deno.land/x/replete/import_map.json" 7 | "https://deno.land/x/replete/replete.js" 8 | "--browser_port=9325" 9 | "--content_type=js:text/javascript" 10 | "--content_type=mjs:text/javascript" 11 | "--content_type=map:application/json" 12 | "--content_type=css:text/css" 13 | "--content_type=html:text/html; charset=utf-8" 14 | "--content_type=wasm:application/wasm" 15 | "--content_type=woff2:font/woff2" 16 | "--content_type=svg:image/svg+xml" 17 | "--content_type=png:image/png" 18 | "--content_type=webp:image/webp")) 19 | (defvar replete-buffer "*replete*") 20 | (defvar replete-process nil) 21 | (defvar replete-remnant "") 22 | 23 | (defun replete-get-config (filename) 24 | 25 | ; Read and parse the project-specific configuration file. 26 | 27 | (json-parse-string 28 | (with-temp-buffer 29 | (insert-file-contents filename) 30 | (buffer-string)) 31 | :object-type 32 | 'hash-table 33 | :array-type 34 | 'list)) 35 | 36 | (defun replete-get-command (filename) 37 | 38 | ; Infer the Replete command, preferring the project-specific command where 39 | ; available. 40 | 41 | (if (file-exists-p filename) 42 | (gethash "command" (replete-get-config filename)) 43 | replete-default-command)) 44 | 45 | (defun replete-errorize (value) 46 | 47 | ; Make a string scary and red. 48 | 49 | (propertize value 'face '(:foreground "red"))) 50 | 51 | (defun replete-append (value after) 52 | 53 | ; Append the string 'value' onto the end of Replete's buffer. By specifying 54 | ; the window-point-insertion-type behaviour, we ensure that the buffer is 55 | ; always scrolled to the end of the output. 56 | 57 | (with-current-buffer replete-buffer 58 | (setq-local window-point-insertion-type t) 59 | (goto-char (point-max)) 60 | (insert value after))) 61 | 62 | (defun replete-result (result) 63 | 64 | ; Handle a result message from Replete. 65 | 66 | (maphash 67 | (lambda (key value) 68 | 69 | ; A result message should contain one of the following keys. If it does not, 70 | ; nothing happens. 71 | 72 | (pcase key 73 | ("out" (replete-append value "")) 74 | ("err" (replete-append (replete-errorize value) "")) 75 | ("exception" (replete-append (replete-errorize value) "\n")) 76 | ("evaluation" (replete-append value "\n")))) 77 | result)) 78 | 79 | (defun replete-consume () 80 | 81 | ; If the remnant contains a newline, then it contains at least one whole 82 | ; message. 83 | 84 | (while (string-match-p (regexp-quote "\n") replete-remnant) 85 | 86 | ; Take the first line from the remnant. 87 | 88 | (let* ((lines (split-string replete-remnant "\n")) 89 | (line (car lines))) 90 | (replete-result 91 | (condition-case nil 92 | 93 | ; Attempt to parse the line as a JSON object. 94 | 95 | (json-parse-string line :object-type 'hash-table) 96 | 97 | ; If the line is not JSON, simulate a result message containing the line as 98 | ; stderr. 99 | 100 | (error 101 | (let ((result (make-hash-table))) 102 | (puthash "err" (concat line "\n") result) 103 | result)))) 104 | 105 | ; The rest of the lines become the new remnant, which is immediately consumed. 106 | 107 | (setq replete-remnant (mapconcat 'identity (cdr lines) "\n"))))) 108 | 109 | (defun replete-chunk (ignore chunk) 110 | 111 | ; Upon reading a chunk of text from Replete's stdout (or stderr), it is 112 | ; appended onto the remnant. Any whole lines within the remnant are then 113 | ; consumed. 114 | 115 | (setq replete-remnant (concat replete-remnant chunk)) 116 | (replete-consume)) 117 | 118 | (defun replete-stop () 119 | 120 | ; Stops the Replete process. 121 | 122 | (interactive) 123 | (if (process-live-p replete-process) 124 | (kill-process replete-process))) 125 | 126 | (defun replete-start (directory) 127 | 128 | ; Starts (or restarts) the Replete process in the given directory. 129 | 130 | (interactive "Ddirectory: ") 131 | (if (process-live-p replete-process) 132 | (replete-stop)) 133 | (setq replete-process 134 | (let ((default-directory directory) 135 | (config-filename (expand-file-name "replete.json" directory))) 136 | (make-process :name "replete" 137 | :buffer replete-buffer 138 | :connection-type 'pipe 139 | :filter #'replete-chunk 140 | :command (replete-get-command config-filename))))) 141 | 142 | (defun replete-eval (platform) 143 | 144 | ; Evaluate the buffer's selected region on the 'platform'. 145 | 146 | (let* ((beg (region-beginning)) 147 | (end (region-end)) 148 | (selected-code (buffer-substring-no-properties beg end))) 149 | 150 | ; Send a command message to Replete's stdin. 151 | 152 | (process-send-string 153 | replete-process 154 | (concat 155 | (json-serialize (list :platform platform 156 | :source selected-code 157 | :locator (concat "file://" (buffer-file-name)) 158 | :scope (buffer-name))) 159 | "\n")))) 160 | 161 | 162 | (defun replete-browser () 163 | (interactive) 164 | (replete-eval "browser")) 165 | 166 | (defun replete-node () 167 | (interactive) 168 | (replete-eval "node")) 169 | 170 | (defun replete-deno () 171 | (interactive) 172 | (replete-eval "deno")) 173 | 174 | (defun replete-bun () 175 | (interactive) 176 | (replete-eval "bun")) 177 | 178 | (defun replete-tjs () 179 | (interactive) 180 | (replete-eval "tjs")) 181 | 182 | (provide 'replete) 183 | -------------------------------------------------------------------------------- /plugins/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replete", 3 | "displayName": "Replete", 4 | "description": "REPL-driven development in JavaScript", 5 | "version": "0.0.16", 6 | "publisher": "jamesdiacono", 7 | "homepage": "https://repletejs.org", 8 | "repository": "https://github.com/jamesdiacono/Replete", 9 | "engines": { 10 | "vscode": "^1.32.0" 11 | }, 12 | "categories": [ 13 | "Notebooks", 14 | "Education", 15 | "Testing", 16 | "Other" 17 | ], 18 | "keywords": [ 19 | "REPL", 20 | "JavaScript", 21 | "interactive", 22 | "RDD", 23 | "eval" 24 | ], 25 | "icon": "./r2d2.png", 26 | "main": "./extension.js", 27 | "activationEvents": [ 28 | "onCommand:replete_start", 29 | "onCommand:replete_stop", 30 | "onCommand:replete_clear", 31 | "onCommand:replete_browser", 32 | "onCommand:replete_node", 33 | "onCommand:replete_deno", 34 | "onCommand:replete_tjs", 35 | "onCommand:replete_bun" 36 | ], 37 | "contributes": { 38 | "commands": [ 39 | { 40 | "command": "replete_start", 41 | "title": "Replete: Start" 42 | }, 43 | { 44 | "command": "replete_stop", 45 | "title": "Replete: Stop" 46 | }, 47 | { 48 | "command": "replete_clear", 49 | "title": "Replete: Clear output" 50 | }, 51 | { 52 | "command": "replete_browser", 53 | "title": "Replete: Evaluate selection in a browser" 54 | }, 55 | { 56 | "command": "replete_node", 57 | "title": "Replete: Evaluate selection in Node.js" 58 | }, 59 | { 60 | "command": "replete_deno", 61 | "title": "Replete: Evaluate selection in Deno" 62 | }, 63 | { 64 | "command": "replete_bun", 65 | "title": "Replete: Evaluate selection in Bun" 66 | }, 67 | { 68 | "command": "replete_tjs", 69 | "title": "Replete: Evaluate selection in Txiki" 70 | } 71 | ], 72 | "keybindings": [ 73 | { 74 | "command": "replete_start", 75 | "key": "alt+r", 76 | "mac": "ctrl+r" 77 | }, 78 | { 79 | "command": "replete_stop", 80 | "key": "alt+s", 81 | "mac": "ctrl+s" 82 | }, 83 | { 84 | "command": "replete_clear", 85 | "key": "alt+l", 86 | "mac": "ctrl+l" 87 | }, 88 | { 89 | "command": "replete_browser", 90 | "key": "alt+b", 91 | "mac": "ctrl+b", 92 | "when": " editorTextFocus" 93 | }, 94 | { 95 | "command": "replete_node", 96 | "key": "alt+n", 97 | "mac": "ctrl+n", 98 | "when": " editorTextFocus" 99 | }, 100 | { 101 | "command": "replete_deno", 102 | "key": "alt+d", 103 | "mac": "ctrl+d", 104 | "when": " editorTextFocus" 105 | }, 106 | { 107 | "command": "replete_bun", 108 | "key": "alt+u", 109 | "mac": "ctrl+u", 110 | "when": " editorTextFocus" 111 | }, 112 | { 113 | "command": "replete_tjs", 114 | "key": "alt+t", 115 | "mac": "ctrl+t", 116 | "when": " editorTextFocus" 117 | } 118 | ], 119 | "configuration": { 120 | "title": "Replete", 121 | "properties": { 122 | "replete.env": { 123 | "type": "object", 124 | "scope": "window", 125 | "default": {}, 126 | "description": "Environment variables provided to the command. You may need to specify a PATH pointing to your runtime binaries, such as 'deno'." 127 | }, 128 | "replete.command": { 129 | "type": "array", 130 | "items": { 131 | "type": "string" 132 | }, 133 | "scope": "window", 134 | "default": [ 135 | "deno", 136 | "run", 137 | "--allow-all", 138 | "--importmap", 139 | "https://deno.land/x/replete/import_map.json", 140 | "https://deno.land/x/replete/replete.js", 141 | "--browser_port=9325", 142 | "--content_type=js:text/javascript", 143 | "--content_type=mjs:text/javascript", 144 | "--content_type=map:application/json", 145 | "--content_type=css:text/css", 146 | "--content_type=html:text/html; charset=utf-8", 147 | "--content_type=wasm:application/wasm", 148 | "--content_type=woff2:font/woff2", 149 | "--content_type=svg:image/svg+xml", 150 | "--content_type=png:image/png", 151 | "--content_type=webp:image/webp" 152 | ], 153 | "description": "Refer to https://github.com/jamesdiacono/Replete/blob/trunk/replete.js for a full list of supported arguments." 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /webl/webl_client.js: -------------------------------------------------------------------------------- 1 | // This module is part of the WEBL suite. It is served to the browser by the 2 | // WEBL server. When it is run, a WEBL is created and hooked up to the server. 3 | // The WEBL may then be operated remotely. 4 | 5 | /*jslint browser */ 6 | 7 | import make_webl from "./webl.js"; 8 | const webl_relay_url = new URL("./webl_relay.js", import.meta.url); 9 | 10 | let webl; 11 | let padawans = Object.create(null); 12 | 13 | // Create a Worker that will be responsible for maintaining the WebSocket 14 | // connection. 15 | 16 | const worker = new Worker(webl_relay_url); 17 | 18 | // Inform the worker of the WebSockets endpoint. 19 | 20 | const websockets_url = ( 21 | location.protocol === "http:" 22 | ? "ws://" 23 | : "wss://" 24 | ) + location.host; 25 | worker.postMessage(websockets_url); 26 | 27 | // Each type of message received from the server invokes a different handler 28 | // function. 29 | 30 | const message_handlers = { 31 | create_padawan(id, spec) { 32 | 33 | // Create a new padawan and let the server know when it is ready. 34 | 35 | function on_log(...values) { 36 | return worker.postMessage({ 37 | type: "status", 38 | name: "log", 39 | value: { 40 | padawan_name: spec.name, 41 | values 42 | } 43 | }); 44 | } 45 | 46 | function on_exception(reason) { 47 | return worker.postMessage({ 48 | type: "status", 49 | name: "exception", 50 | value: { 51 | padawan_name: spec.name, 52 | reason 53 | } 54 | }); 55 | } 56 | 57 | padawans[spec.name] = webl.padawan(Object.assign( 58 | {on_log, on_exception}, 59 | spec 60 | )); 61 | return padawans[spec.name].create().then( 62 | function on_success() { 63 | return worker.postMessage({ 64 | type: "response", 65 | request_id: id, 66 | value: true 67 | }); 68 | }, 69 | function on_fail(exception) { 70 | return on_exception(exception.stack); 71 | } 72 | ); 73 | }, 74 | eval_module(id, {script, imports, wait, padawan_name}) { 75 | 76 | // Give a padawan some source code to evaluate, then transmit the 77 | // resulting value to the server. 78 | 79 | const padawan = padawans[padawan_name]; 80 | if (padawan === undefined) { 81 | return worker.postMessage({ 82 | type: "response", 83 | request_id: id, 84 | reason: { 85 | code: "padawan_not_found", 86 | evidence: padawan_name 87 | } 88 | }); 89 | } 90 | return padawan.eval(script, imports, wait).then( 91 | function on_success(report) { 92 | return worker.postMessage({ 93 | type: "response", 94 | request_id: id, 95 | value: report 96 | }); 97 | }, 98 | function on_fail(exception) { 99 | return worker.postMessage({ 100 | type: "status", 101 | name: "exception", 102 | value: { 103 | padawan_name, 104 | reason: exception.stack 105 | } 106 | }); 107 | } 108 | ); 109 | }, 110 | destroy_padawan(id, {name}) { 111 | 112 | // Destroy a padawan, if it exists. 113 | 114 | if (padawans[name] !== undefined) { 115 | padawans[name].destroy(); 116 | delete padawans[name]; 117 | } 118 | return worker.postMessage({ 119 | type: "response", 120 | request_id: id, 121 | value: true 122 | }); 123 | } 124 | }; 125 | worker.onmessage = function (event) { 126 | if (typeof event.data === "boolean") { 127 | if (event.data) { 128 | 129 | // The connection has been opened. 130 | 131 | if (webl === undefined) { 132 | webl = make_webl(); 133 | addEventListener("beforeunload", webl.destroy); 134 | document.title = "WEBL"; 135 | worker.postMessage({ 136 | type: "ready", 137 | value: location.origin 138 | }); 139 | } else { 140 | 141 | // The server is back up. Reload the page to clear any global state (for 142 | // example, modifications to the DOM or window object made by a "top" padawan). 143 | // Following the reload a new connection will be attempted. 144 | 145 | // Firefox, unlike other browsers, fires the "close" event on any open 146 | // WebSockets as the page unloads. The relay worker responds to a "close" event 147 | // by attempting to reestablish a WebSocket connection, and this appears to 148 | // occasionally leave behind some sort of "ghost" WebSocket that kicks the page 149 | // into an infinite reload loop. To suppress any doomed reconnection attempts, 150 | // we terminate the relay worker before unloading the page. 151 | 152 | worker.terminate(); 153 | location.reload(); 154 | } 155 | } else { 156 | 157 | // The connection has been closed. Destroy the WEBL (to avoid the possibility of 158 | // orphaned padawans) and wait for the connection with the server to be 159 | // repaired. 160 | 161 | if (webl !== undefined) { 162 | webl.destroy(); 163 | } 164 | document.title = "Reconnecting..."; 165 | } 166 | } else { 167 | 168 | // A message has been received from the server. 169 | 170 | message_handlers[event.data.name]( 171 | event.data.id, 172 | event.data.parameters 173 | ); 174 | } 175 | }; 176 | document.title = "Connecting..."; 177 | -------------------------------------------------------------------------------- /plugins/nrepl/bencode.js: -------------------------------------------------------------------------------- 1 | // An encoder and decoder for Bencode, a fairly minimal encoding for structured 2 | // data. Byte strings are assumed to be UTF-8 encoded text. 3 | 4 | // See https://en.wikipedia.org/wiki/Bencode. 5 | // Also https://github.com/nrepl/bencode/blob/master/src/bencode/core.clj. 6 | 7 | /*jslint web */ 8 | 9 | const to = new TextEncoder("utf-8"); 10 | const from = new TextDecoder("utf-8", {fatal: true}); 11 | const minus = 45; 12 | const zero = 48; 13 | const nine = 57; 14 | const colon = 58; 15 | const d = 100; 16 | const e = 101; 17 | const i = 105; 18 | const l = 108; 19 | 20 | function concat_bytes(a, b) { 21 | let array = new Uint8Array(a.byteLength + b.byteLength); 22 | array.set(a, 0); 23 | array.set(b, a.byteLength); 24 | return array; 25 | } 26 | 27 | function encode(value) { 28 | if (Number.isSafeInteger(value)) { 29 | return to.encode("i" + value + "e"); 30 | } 31 | if (typeof value === "string") { 32 | const bytes = to.encode(value); 33 | return concat_bytes( 34 | to.encode(String(bytes.byteLength) + ":"), 35 | bytes 36 | ); 37 | } 38 | if (Array.isArray(value)) { 39 | return [ 40 | to.encode("l"), 41 | ...value.map(encode), 42 | to.encode("e") 43 | ].reduce(concat_bytes); 44 | } 45 | if (typeof value === "object" && value) { 46 | return [ 47 | to.encode("d"), 48 | ...Object.keys(value).sort().flatMap(function (key) { 49 | return ( 50 | value[key] !== undefined 51 | ? [encode(key), encode(value[key])] 52 | : [] 53 | ); 54 | }), 55 | to.encode("e") 56 | ].reduce(concat_bytes); 57 | } 58 | throw new Error("Unsupported value '" + value + "'."); 59 | } 60 | 61 | function decode_positive_integer(bytes, start) { 62 | let end = start; 63 | while (bytes[end] >= zero && bytes[end] <= nine) { 64 | end += 1; 65 | } 66 | const digits = from.decode(bytes.slice(start, end)); 67 | const value = parseInt(digits); 68 | if (!Number.isSafeInteger(value)) { 69 | throw new Error("Expected digits, got '" + digits + "'."); 70 | } 71 | return [value, end]; 72 | } 73 | 74 | function decode_from(bytes, at) { 75 | if (bytes[at] === i) { 76 | 77 | // Signed integer. 78 | 79 | at += 1; 80 | let sign = 1; 81 | if (bytes[at] === minus) { 82 | sign = -1; 83 | at += 1; 84 | } 85 | let [integer, end] = decode_positive_integer(bytes, at); 86 | if (end >= bytes.byteLength) { 87 | throw new Error("Unexpected EOF."); 88 | } 89 | if (bytes[end] !== e) { 90 | throw new Error("Expected 'e'."); 91 | } 92 | return [ 93 | sign * integer, 94 | end + 1 95 | ]; 96 | } 97 | if (bytes[at] === l) { 98 | 99 | // List. 100 | 101 | at += 1; 102 | let element; 103 | let array = []; 104 | while (bytes[at] !== e && at < bytes.byteLength) { 105 | [element, at] = decode_from(bytes, at); 106 | array.push(element); 107 | } 108 | if (at >= bytes.byteLength) { 109 | throw new Error("Unexpected EOF."); 110 | } 111 | return [ 112 | array, 113 | at + 1 114 | ]; 115 | } 116 | if (bytes[at] === d) { 117 | 118 | // Dictionary. 119 | 120 | at += 1; 121 | let key; 122 | let value; 123 | let object = {}; 124 | while (bytes[at] !== e && at < bytes.byteLength) { 125 | [key, at] = decode_from(bytes, at); 126 | if (typeof key !== "string") { 127 | throw new Error("Expected string key."); 128 | } 129 | [value, at] = decode_from(bytes, at); 130 | object[key] = value; 131 | } 132 | if (at >= bytes.byteLength) { 133 | throw new Error("Unexpected EOF."); 134 | } 135 | return [ 136 | object, 137 | at + 1 138 | ]; 139 | } 140 | 141 | // String. 142 | 143 | let length; 144 | [length, at] = decode_positive_integer(bytes, at); 145 | if (bytes[at] !== colon) { 146 | throw new Error("Expected ':'."); 147 | } 148 | at += 1; 149 | const begin = at; 150 | at += length; 151 | if (at > bytes.byteLength) { 152 | throw new Error("Unexpected EOF."); 153 | } 154 | return [ 155 | from.decode(bytes.slice(begin, at)), 156 | at 157 | ]; 158 | } 159 | 160 | function decode(bytes) { 161 | const [value, at] = decode_from(bytes, 0); 162 | if (at !== bytes.byteLength) { 163 | throw new Error("Unexpected EOF."); 164 | } 165 | return value; 166 | } 167 | 168 | function roundtrips(value) { 169 | return JSON.stringify(decode(encode(value))) === JSON.stringify(value); 170 | } 171 | 172 | function throws(callback) { 173 | try { 174 | callback(); 175 | return false; 176 | } catch (_) { 177 | return true; 178 | } 179 | } 180 | 181 | if (import.meta.main) { 182 | if ( 183 | !roundtrips(0) 184 | || !roundtrips(42) 185 | || !roundtrips(-42) 186 | || !roundtrips("") 187 | || !roundtrips("big 🍌") 188 | || !roundtrips([]) 189 | || !roundtrips([1, 2, 3]) 190 | || !roundtrips([1, [2], 3]) 191 | || !roundtrips({a: 0, b: [1, 2], c: {d: "3"}}) 192 | || roundtrips({b: 0, a: 2}) // key ordering is lost 193 | || encode({a: 0, b: undefined}).join() !== encode({a: 0}).join() 194 | || !throws(() => encode(undefined)) 195 | || !throws(() => encode(NaN)) 196 | || !throws(() => encode(Infinity)) 197 | || !throws(() => encode(true)) 198 | || !throws(() => encode({a: true})) 199 | ) { 200 | throw new Error("FAIL"); 201 | } 202 | } 203 | 204 | export default Object.freeze({encode, decode, decode_from}); 205 | -------------------------------------------------------------------------------- /browser_repl.js: -------------------------------------------------------------------------------- 1 | // This REPL evaluates JavaScript source code in a browser environment. 2 | 3 | /*jslint node */ 4 | 5 | import make_repl from "./repl.js"; 6 | import make_webl_server from "./webl/webl_server.js"; 7 | 8 | function make_browser_repl( 9 | capabilities, 10 | port, 11 | hostname = "localhost", 12 | padawan_type = "top", 13 | humanoid = false 14 | ) { 15 | 16 | // The 'make_browser_repl' function takes several parameters: 17 | 18 | // capabilities 19 | // An object containing the standard Replete capability functions. 20 | 21 | // port 22 | // The port number of the WEBL server. If undefined, an unallocated 23 | // port will be chosen automatically. 24 | 25 | // hostname 26 | // The hostname of the WEBL server. 27 | 28 | // padawan_type 29 | // The type of the padawan, see ./webl/webl.js. 30 | 31 | // humanoid 32 | // A boolean indicating whether to use C3PO as a favicon, rather than 33 | // R2D2. 34 | 35 | // Configure the WEBL server. 36 | 37 | let clients = []; 38 | let padawans = new WeakMap(); 39 | 40 | function create_padawan(client) { 41 | const padawan = client.padawan({ 42 | on_log(...strings) { 43 | return capabilities.out(strings.join(" ") + "\n"); 44 | }, 45 | on_exception(string) { 46 | return capabilities.err(string + "\n"); 47 | }, 48 | type: padawan_type, 49 | 50 | // If the padawan is rendered as an iframe, it fills the WEBL client's 51 | // viewport. We set block display to avoid vertical scrolling. 52 | 53 | iframe_style_object: { 54 | border: "none", 55 | width: "100vw", 56 | height: "100vh", 57 | display: "block" 58 | }, 59 | iframe_sandbox: false 60 | }); 61 | padawans.set(client, padawan); 62 | return padawan.create().catch(function (exception) { 63 | return capabilities.err(exception.stack + "\n"); 64 | }); 65 | } 66 | 67 | function on_client_found(client) { 68 | capabilities.out("WEBL found.\n"); 69 | clients.push(client); 70 | 71 | // Create a single padawan on each connecting client. 72 | 73 | return create_padawan(client); 74 | } 75 | 76 | function on_client_lost(client) { 77 | capabilities.out("WEBL lost.\n"); 78 | 79 | // Forget the client. 80 | 81 | clients = clients.filter(function (a_client) { 82 | return a_client !== client; 83 | }); 84 | } 85 | 86 | let webl_server; 87 | let repl; 88 | 89 | function on_start() { 90 | webl_server = make_webl_server( 91 | function on_exception(error) { 92 | return capabilities.err(error.stack + "\n"); 93 | }, 94 | on_client_found, 95 | on_client_lost, 96 | function on_request(req, res) { 97 | return repl.serve( 98 | new URL(req.url, "http://" + req.host).href, 99 | req.headers 100 | ).then(function ({body, headers}) { 101 | Object.entries(headers).forEach(function ([key, value]) { 102 | res.setHeader(key, value); 103 | }); 104 | res.end(body); 105 | }).catch(function fail(reason) { 106 | capabilities.err(reason.stack + "\n"); 107 | res.statusCode = 500; 108 | return res.end(); 109 | }); 110 | }, 111 | humanoid 112 | ); 113 | return webl_server.start(port, hostname).then(function (actual_port) { 114 | port = actual_port; 115 | capabilities.out( 116 | "Waiting for WEBL: http://" + ( 117 | 118 | // IPv6 addresses must be wrapped in square brackets to appear in a URL. 119 | 120 | hostname.includes(":") 121 | ? "[" + hostname + "]" 122 | : hostname 123 | ) + ":" + port + "\n" 124 | ); 125 | }); 126 | } 127 | 128 | function on_stop() { 129 | return webl_server.stop(); 130 | } 131 | 132 | function on_eval( 133 | on_result, 134 | produce_script, 135 | dynamic_specifiers, 136 | import_specifiers, 137 | wait 138 | ) { 139 | 140 | // Evaluates the module in many padawans at once. Results are reported back as 141 | // they arrive. 142 | 143 | if (clients.length === 0) { 144 | return Promise.reject(new Error("No WEBLs connected.")); 145 | } 146 | return Promise.all( 147 | clients.map(function (client) { 148 | 149 | function qualify(specifier) { 150 | 151 | // Generally, padawans have a different origin to that of the WEBL client. This 152 | // means that absolute paths might be resolved against an unexpected origin. To 153 | // avoid this hazard, each absolute path is converted to a fully-qualified URL 154 | // by prepending the client's origin. 155 | 156 | return ( 157 | specifier.startsWith("/") 158 | ? client.origin + specifier 159 | : specifier 160 | ); 161 | } 162 | 163 | return padawans.get(client).eval( 164 | produce_script(dynamic_specifiers.map(qualify)), 165 | import_specifiers.map(qualify), 166 | wait 167 | ).then(function (report) { 168 | return on_result(report.evaluation, report.exception); 169 | }); 170 | }) 171 | ); 172 | } 173 | 174 | function specify(locator) { 175 | 176 | // If the locator is a file URL, we convert it to an absolute path. This is then 177 | // fully qualified in 'on_eval' above. 178 | 179 | return ( 180 | locator.startsWith("file:///") 181 | ? locator.replace("file://", "") 182 | : locator 183 | ); 184 | } 185 | 186 | repl = make_repl( 187 | capabilities, 188 | on_start, 189 | on_eval, 190 | on_stop, 191 | specify 192 | ); 193 | return Object.freeze({ 194 | start: repl.start, 195 | send: repl.send, 196 | stop: repl.stop 197 | }); 198 | } 199 | 200 | export default Object.freeze(make_browser_repl); 201 | -------------------------------------------------------------------------------- /plugins/vscode/extension.js: -------------------------------------------------------------------------------- 1 | /*jslint node */ 2 | 3 | const child_process = require("node:child_process"); 4 | const fs = require("node:fs"); 5 | const path = require("node:path"); 6 | const process = require("node:process"); 7 | const readline = require("node:readline"); 8 | const url = require("node:url"); 9 | const {window, commands, workspace} = require("vscode"); 10 | 11 | const colors = { 12 | browser: "rgba(255, 242, 130, 0.3)", // yellow 13 | node: "rgba(60, 135, 58, 0.5)", // green 14 | deno: "rgba(255, 255, 255, 0.5)", // white 15 | bun: "rgba(254, 187, 207, 0.5)", // pink 16 | tjs: "rgba(76, 89, 207, 0.5)" // blue 17 | }; 18 | 19 | let replete; 20 | let line_reader; 21 | let output_channel; 22 | 23 | function stop() { 24 | 25 | // Stop the Replete process and dispose of any related resources. 26 | 27 | if (replete !== undefined) { 28 | output_channel.dispose(); 29 | line_reader.close(); 30 | replete.kill(); 31 | replete = undefined; 32 | } 33 | } 34 | 35 | function read_config() { 36 | 37 | // Look for a replete.json in the workspace root. If found, merge it with the 38 | // user config. If anything goes wrong, just produce the user config. 39 | 40 | const user_config = workspace.getConfiguration("replete"); 41 | if (typeof workspace.rootPath !== "string") { 42 | return Promise.resolve(user_config); 43 | } 44 | return fs.promises.readFile( 45 | path.join(workspace.rootPath, "replete.json"), 46 | "utf8" 47 | ).then( 48 | JSON.parse 49 | ).then(function (project_config) { 50 | return { 51 | command: project_config.command ?? user_config.command, 52 | env: Object.assign({}, user_config.env, project_config.env) 53 | }; 54 | }).catch(function () { 55 | return user_config; 56 | }); 57 | } 58 | 59 | function start() { 60 | 61 | // Start the Replete process, or restart it if it is already running. 62 | 63 | stop(); 64 | 65 | // Create a channel to display Replete's output. It will appear in the "Output" 66 | // window. 67 | 68 | output_channel = window.createOutputChannel("Replete"); 69 | output_channel.show(true); 70 | 71 | // Spawn the Replete process. 72 | 73 | return read_config().then(function (config) { 74 | const [command, ...args] = config.command; 75 | replete = child_process.spawn(command, args, { 76 | cwd: workspace.rootPath, 77 | 78 | // Deno's colorful output is not rendered correctly, so suppress it. 79 | 80 | env: Object.assign({}, process.env, {NO_COLOR: "1"}, config.env) 81 | }); 82 | replete.on("error", function (error) { 83 | return output_channel.append(error.stack); 84 | }); 85 | 86 | // Listen for Replete's result messages on STDOUT. Each line is a JSON-encoded 87 | // message. 88 | 89 | line_reader = readline.createInterface({input: replete.stdout}); 90 | line_reader.on("line", function (line) { 91 | 92 | // Exactly one of these four properties will be defined. Evaluations and 93 | // exceptions do not come with a trailing newline. 94 | 95 | const {out, err, evaluation, exception} = JSON.parse(line); 96 | const text = out ?? err ?? evaluation ?? exception; 97 | return output_channel.append( 98 | (evaluation !== undefined || exception !== undefined) 99 | ? text + "\n" 100 | : text 101 | ); 102 | }); 103 | 104 | // Listen for any problems with Replete itself on STDERR. 105 | 106 | replete.stderr.setEncoding("utf8"); 107 | replete.stderr.on("data", function (chunk) { 108 | return output_channel.append(chunk); 109 | }); 110 | }); 111 | } 112 | 113 | function clear() { 114 | if (output_channel !== undefined) { 115 | output_channel.clear(); 116 | } 117 | } 118 | 119 | function evaluate(platform) { 120 | 121 | // Evaluate the currently selected source code in the 'platform' REPL. 122 | 123 | if (replete === undefined) { 124 | throw new Error("Replete is not running."); 125 | } 126 | const editor = window.activeTextEditor; 127 | 128 | // Any empty selections are expanded to fill the line. 129 | 130 | const selections = editor.selections.map(function (selection) { 131 | return ( 132 | selection.isEmpty 133 | ? editor.document.lineAt(selection.start).range 134 | : selection 135 | ); 136 | }); 137 | 138 | // Briefly highlight the selected text. 139 | 140 | const decoration = window.createTextEditorDecorationType({ 141 | backgroundColor: colors[platform], 142 | borderRadius: "3px" 143 | }); 144 | window.activeTextEditor.setDecorations(decoration, selections); 145 | setTimeout(decoration.dispose, 300); 146 | 147 | // Extract the selected source code from the document. Selections are provided 148 | // in the order that they were created, so we first sort them in the order they 149 | // appear on the page before amalgamating them. 150 | 151 | const source = selections.sort(function (a, b) { 152 | return ( 153 | a.start.line - b.start.line 154 | || a.start.character - b.start.character 155 | ); 156 | }).map(function (selection) { 157 | return editor.document.getText(selection); 158 | }).join("\n"); 159 | 160 | // Send a command message to Replete. 161 | 162 | replete.stdin.write(JSON.stringify({ 163 | source, 164 | locator: ( 165 | !editor.document.isUntitled 166 | ? url.pathToFileURL(editor.document.fileName) 167 | : undefined 168 | ), 169 | scope: editor.document.fileName, 170 | platform 171 | }) + "\n"); 172 | 173 | // Show the output pane in anticipation of a result. 174 | 175 | output_channel.show(true); 176 | } 177 | 178 | function activate(context) { 179 | context.subscriptions.push( 180 | commands.registerCommand("replete_start", start), 181 | commands.registerCommand("replete_stop", stop), 182 | commands.registerCommand("replete_clear", clear), 183 | commands.registerCommand("replete_browser", () => evaluate("browser")), 184 | commands.registerCommand("replete_node", () => evaluate("node")), 185 | commands.registerCommand("replete_deno", () => evaluate("deno")), 186 | commands.registerCommand("replete_tjs", () => evaluate("tjs")), 187 | commands.registerCommand("replete_bun", () => evaluate("bun")) 188 | ); 189 | } 190 | 191 | module.exports = { 192 | activate, 193 | deactivate: stop 194 | }; 195 | -------------------------------------------------------------------------------- /webl/webl_inspect.js: -------------------------------------------------------------------------------- 1 | // Format any value as a nice readable string. Useful for debugging. 2 | 3 | // Values nested within 'value' are inspected no deeper than 'maximum_depth' 4 | // levels. 5 | 6 | // The inspected depth is automatically constrained such that the returned 7 | // string is no longer than 'maximum_length'. 8 | 9 | /*jslint browser, global, null */ 10 | 11 | function inspect(value, maximum_depth = 10, maximum_length = 65536) { 12 | 13 | function is_primitive(value) { 14 | return ( 15 | value === undefined 16 | || value === null 17 | || typeof value === "boolean" 18 | || typeof value === "number" 19 | || typeof value === "string" 20 | ); 21 | } 22 | 23 | let dent = ""; 24 | 25 | function indent() { 26 | dent += " "; 27 | } 28 | 29 | function outdent() { 30 | dent = dent.slice(4); 31 | } 32 | 33 | // The string is built up as the value is traversed. 34 | 35 | let string = ""; 36 | 37 | function write(fragment) { 38 | string += fragment; 39 | } 40 | 41 | function too_long() { 42 | return string.length > maximum_length; 43 | } 44 | 45 | (function print(value, depth = 0, ancestors = []) { 46 | if (typeof value === "function") { 47 | return write("[Function: " + (value.name || "(anonymous)") + "]"); 48 | } 49 | if (typeof value === "string") { 50 | 51 | // Add quotes around strings, and encode any newlines. 52 | 53 | return write(JSON.stringify(value)); 54 | } 55 | if ( 56 | is_primitive(value) 57 | || value.constructor === RegExp 58 | || value.constructor === Symbol 59 | ) { 60 | return write(String(value)); 61 | } 62 | if (typeof value !== "object") { 63 | 64 | // BigInt, etc. 65 | 66 | return write( 67 | "[" + value.constructor.name + ": " + String(value) + "]" 68 | ); 69 | } 70 | if (value.constructor === Date) { 71 | return write("[Date: " + value.toJSON() + "]"); 72 | } 73 | 74 | // We keep track of object-like values that have already been (or are being) 75 | // printed, otherwise we would be at risk of entering an infinite loop. 76 | 77 | if (ancestors.includes(value)) { 78 | return write("[Circular]"); 79 | } 80 | ancestors = [...ancestors, value]; 81 | 82 | function print_member(key, value, compact, last) { 83 | 84 | // The 'print_member' function prints out an element of an array, or property of 85 | // an object. 86 | 87 | if (!compact) { 88 | write("\n" + dent); 89 | } 90 | if (key !== undefined) { 91 | write(key + ": "); 92 | } 93 | print(value, depth + 1, ancestors); 94 | if (!last) { 95 | return write( 96 | compact 97 | ? ", " 98 | : "," 99 | ); 100 | } 101 | if (!compact) { 102 | return write("\n" + dent.slice(4)); 103 | } 104 | } 105 | 106 | const leaf = depth >= maximum_depth; 107 | if (Array.isArray(value)) { 108 | if (leaf) { 109 | return write("[Array]"); 110 | } 111 | const compact = value.length < 3 && value.every(is_primitive); 112 | write("["); 113 | indent(); 114 | value.every(function (element, element_nr) { 115 | 116 | // Exiting early prevents memory exhaustion when inspecting enormous values. 117 | 118 | if (too_long()) { 119 | return false; 120 | } 121 | print_member( 122 | undefined, 123 | element, 124 | compact, 125 | element_nr === value.length - 1 126 | ); 127 | return true; 128 | }); 129 | outdent(); 130 | return write("]"); 131 | } 132 | 133 | // The value is an object. Print out its properties. 134 | 135 | if (value.constructor === undefined) { 136 | 137 | // The object has no prototype. A descriptive prefix might be helpful. 138 | 139 | write("[Object: null prototype]"); 140 | if (leaf) { 141 | return; 142 | } 143 | write(" "); 144 | } else { 145 | if (leaf) { 146 | return write("[" + value.constructor.name + "]"); 147 | } 148 | if (value.constructor !== Object) { 149 | 150 | // The object has an unusual prototype. Give it a descriptive prefix. 151 | 152 | write("[" + value.constructor.name + "] "); 153 | } 154 | 155 | // Some kinds of objects are better represented as an array, but only if the 156 | // iterator is well behaved. 157 | 158 | if (value[Symbol.iterator] !== undefined) { 159 | try { 160 | return print(Array.from(value), depth, ancestors); 161 | } catch (_) {} 162 | } 163 | } 164 | write("{"); 165 | indent(); 166 | 167 | // Non-enumerable properties, such as the innumerable DOM element methods, are 168 | // omitted because they overwhelm the output. 169 | 170 | const keys = Object.keys(value); 171 | keys.every(function (key, key_nr) { 172 | if (too_long()) { 173 | return false; 174 | } 175 | 176 | // It is possible that the property is a getter, and that it will fail when 177 | // accessed. Omit any malfunctioning properties without affecting the others. 178 | 179 | let property_value; 180 | try { 181 | property_value = value[key]; 182 | } catch (_) {} 183 | print_member( 184 | key, 185 | property_value, 186 | keys.length === 1 && is_primitive(property_value), 187 | key_nr === keys.length - 1 188 | ); 189 | return true; 190 | }); 191 | outdent(); 192 | return write("}"); 193 | }(value)); 194 | if (too_long() && maximum_depth > 0) { 195 | return inspect(value, maximum_depth - 1, maximum_length); 196 | } 197 | return string.slice(0, maximum_length); 198 | } 199 | 200 | if (import.meta.main) { 201 | const not_circular = {}; 202 | let circular = Object.create(null); 203 | circular.self = circular; 204 | let bad_iterator = {}; 205 | bad_iterator[Symbol.iterator] = "BOOM"; 206 | if ( 207 | inspect() !== "undefined" 208 | || inspect(null) !== "null" 209 | || inspect(123) !== "123" 210 | || inspect(Infinity) !== "Infinity" 211 | || inspect(NaN) !== "NaN" 212 | || inspect([1, {"2": [3, 4]}]) !== `[ 213 | 1, 214 | { 215 | 2: [3, 4] 216 | } 217 | ]` 218 | || inspect(["a", {"b": "c"}], 1) !== `[ 219 | "a", 220 | [Object] 221 | ]` 222 | || inspect([1, 2], 0) !== "[Array]" 223 | || inspect([1, {"2": [3, 4]}], 2, 30) !== `[ 224 | 1, 225 | [Object] 226 | ]` 227 | || inspect([1, {"2": [3, 4]}], 2, 5) !== "[Arra" 228 | || inspect(new Uint8Array([0, 255])) !== "[Uint8Array] [0, 255]" 229 | || inspect(Math.random) !== "[Function: random]" 230 | || inspect([not_circular, not_circular]) !== `[ 231 | {}, 232 | {} 233 | ]` 234 | || inspect(circular) !== `[Object: null prototype] { 235 | self: [Circular] 236 | }` 237 | || inspect(bad_iterator) !== "{}" 238 | ) { 239 | throw new Error("FAIL"); 240 | } 241 | const huge = (function array_bomb(depth = 20, cache = []) { 242 | if (cache[depth] === undefined) { 243 | cache[depth] = new Array(depth).fill().map(function () { 244 | return array_bomb(depth - 1, cache); 245 | }); 246 | } 247 | return cache[depth]; 248 | }()); 249 | inspect(huge); // requires correctly functioning early exit 250 | } 251 | 252 | export default Object.freeze(inspect); 253 | -------------------------------------------------------------------------------- /webl/c3po.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 25 | 32 | 40 | 42 | 48 | 51 | 61 | 68 | 70 | 80 | 87 | 94 | 95 | 101 | 106 | 114 | 118 | 119 | 131 | 132 | 139 | 144 | 152 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /plugins/neovim/lua/replete.lua: -------------------------------------------------------------------------------- 1 | local json = require "json" 2 | 3 | local function split(text) 4 | 5 | -- Splits the 'text' string at its first linebreak, returning the two resulting 6 | -- strings. If the 'text' did not contain a linebreak, the second return value 7 | -- will be nil. 8 | 9 | local start_nr, end_nr = text:find("[\r\n]+") 10 | if start_nr == nil then 11 | return text 12 | end 13 | return text:sub(0, start_nr - 1), text:sub(end_nr + 1) 14 | end 15 | 16 | local output_buffer 17 | local function append(text) 18 | 19 | -- Write the 'text' string to end of the output buffer. 20 | 21 | local trailing = vim.api.nvim_buf_get_lines(output_buffer, -2, -1, true)[1] 22 | local line, rest = split(text) 23 | if rest == nil then 24 | 25 | -- The text contains a single line. Append it to the last line in the buffer. 26 | 27 | vim.api.nvim_buf_set_lines( 28 | output_buffer, 29 | -2, 30 | -1, 31 | true, 32 | {trailing .. line} 33 | ) 34 | 35 | -- Scroll the bottom of the output buffer into view. 36 | 37 | return vim.api.nvim_buf_call( 38 | output_buffer, 39 | function () 40 | vim.cmd("normal G") 41 | end 42 | ) 43 | end 44 | 45 | -- The text contains at least a whole line. Append this line to the last 46 | -- line in the buffer, followed by an empty line. 47 | 48 | vim.api.nvim_buf_set_lines( 49 | output_buffer, 50 | -2, 51 | -1, 52 | true, 53 | {trailing .. line, ""} 54 | ) 55 | 56 | -- Then append the rest of the lines. 57 | 58 | return append(rest) 59 | end 60 | 61 | local function consume(text) 62 | 63 | -- Process any messages in the 'text' string, returning the leftovers. 64 | 65 | local line, rest = split(text) 66 | if rest == nil then 67 | 68 | -- If the text does not contain a linebreak, then it does not yet contain a 69 | -- whole message. Return the leftover string. 70 | 71 | return text 72 | end 73 | 74 | -- Parse the line into a message. 75 | 76 | local message = json.decode(line) 77 | 78 | -- The message will contain one of the following keys. The last two do not come 79 | -- with a trailing newline, so we supply one. 80 | 81 | if message["out"] ~= nil then 82 | append(message["out"]) 83 | elseif message["err"] ~= nil then 84 | append(message["err"]) 85 | elseif message["evaluation"] ~= nil then 86 | append(message["evaluation"] .. "\n") 87 | elseif message["exception"] ~= nil then 88 | append(message["exception"] .. "\n") 89 | end 90 | 91 | -- Consume any other messages in the text. 92 | 93 | return consume(rest) 94 | end 95 | 96 | local replete 97 | local function stop() 98 | 99 | -- Stops Replete and closes its window. 100 | 101 | if output_buffer == nil then 102 | return 103 | end 104 | if replete ~= nil then 105 | replete:kill(15) -- SIGTERM 106 | end 107 | 108 | -- Close the output buffer and its window. 109 | 110 | vim.api.nvim_buf_call( 111 | output_buffer, 112 | function () 113 | vim.cmd("bdelete") 114 | end 115 | ) 116 | output_buffer = nil 117 | end 118 | 119 | local output_window 120 | local path_separator = package.config:sub(1,1) 121 | local stdin 122 | local remnant = "" 123 | local function start() 124 | 125 | -- Starts (or restarts) Replete. 126 | 127 | stop() 128 | 129 | -- Split the window, creating a scratch buffer to the right of the current 130 | -- buffer. This buffer will display Replete's output as it arrives. 131 | 132 | vim.cmd("vsplit") 133 | output_buffer = vim.api.nvim_create_buf(true, true) 134 | local windows = vim.api.nvim_list_wins() 135 | output_window = windows[#windows] 136 | vim.api.nvim_win_set_buf(output_window, output_buffer) 137 | 138 | -- Read the project-specific configuration file. 139 | 140 | local path = vim.fn.getcwd() .. path_separator .. "replete.json" 141 | local file = io.open(path, "r") 142 | local command 143 | if file then 144 | local content = file:read("*a") 145 | command = json.decode(content).command 146 | file:close() 147 | end 148 | 149 | -- Start the Replete process. 150 | 151 | local stdout = vim.loop.new_pipe() 152 | local stderr = vim.loop.new_pipe() 153 | stdin = vim.loop.new_pipe() 154 | local function destroy() 155 | stdin:close() 156 | stdout:close() 157 | stderr:close() 158 | if replete ~= nil then 159 | replete:close() 160 | end 161 | end 162 | local command_array = command or vim.g.replete_command or { 163 | "deno", 164 | "run", 165 | "--allow-all", 166 | "--importmap", 167 | "https://deno.land/x/replete/import_map.json", 168 | "https://deno.land/x/replete/replete.js", 169 | "--browser_port=9325", 170 | "--content_type=js:text/javascript", 171 | "--content_type=mjs:text/javascript", 172 | "--content_type=map:application/json", 173 | "--content_type=css:text/css", 174 | "--content_type=html:text/html; charset=utf-8", 175 | "--content_type=wasm:application/wasm", 176 | "--content_type=woff2:font/woff2", 177 | "--content_type=svg:image/svg+xml", 178 | "--content_type=png:image/png", 179 | "--content_type=webp:image/webp" 180 | } 181 | local cmd = command_array[1] 182 | local args = {unpack(command_array, 2)} 183 | replete = vim.loop.spawn( 184 | cmd, 185 | { 186 | args = args, 187 | stdio = {stdin, stdout, stderr}, 188 | cwd = vim.g.replete_cwd 189 | }, 190 | destroy 191 | ) 192 | if replete == nil then 193 | append("Failed to start " .. cmd .. ".") 194 | return destroy() 195 | end 196 | 197 | -- Listen for Replete's result messages on STDOUT. Each line is a JSON-encoded 198 | -- message. A nil chunk indicates the end of the stream. 199 | 200 | stdout:read_start(vim.schedule_wrap( 201 | function (error, chunk) 202 | if error ~= nil then 203 | return append(error) 204 | end 205 | if chunk ~= nil then 206 | 207 | -- When a chunk of text arrives from Replete's STDOUT, it is appended to any 208 | -- remnant characters which arrived after the last message. If the resulting 209 | -- string contains any messages, they are removed and processed. Any leftovers 210 | -- become the new remnant. 211 | 212 | remnant = consume(remnant .. chunk) 213 | end 214 | end 215 | )) 216 | 217 | -- Listen for any problems with Replete itself on STDERR. 218 | 219 | stderr:read_start(vim.schedule_wrap( 220 | function (ignore, chunk) 221 | if chunk ~= nil then 222 | append(chunk) 223 | end 224 | end 225 | )) 226 | end 227 | 228 | local function eval_selection(platform) 229 | 230 | -- Evaluate the currently selected source code of the active buffer on the 231 | -- specified 'platform'. 232 | 233 | if output_buffer == nil then 234 | return 235 | end 236 | 237 | -- Recover the visual selection (gv), and yank it to the 'a' register ("ay). 238 | 239 | vim.cmd("normal! gv\"ay") 240 | 241 | -- Send a command message to the Replete process. 242 | 243 | local buffer = vim.api.nvim_get_current_buf() 244 | 245 | -- Determine the locator, which is a file URL. 246 | 247 | local locator = ( 248 | "file://" 249 | .. vim.api.nvim_buf_get_name( 250 | buffer 251 | ):gsub( 252 | 253 | -- Convert Windows-style path delimiters. 254 | 255 | "\\", 256 | "/" 257 | ):gsub( 258 | 259 | -- Ensure a leading slash in the filename. 260 | 261 | "^/?", 262 | "/" 263 | ) 264 | ) 265 | stdin:write( 266 | json.encode({ 267 | source = vim.fn.getreg("a"), 268 | locator = locator, 269 | platform = platform, 270 | scope = tostring(buffer) 271 | }) 272 | .. "\n" 273 | ) 274 | end 275 | 276 | local function toggle() 277 | if output_buffer == nil then 278 | start() 279 | else 280 | stop() 281 | end 282 | end 283 | 284 | return { 285 | start = start, 286 | stop = stop, 287 | toggle = toggle, 288 | eval = eval_selection 289 | } 290 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | // This module exports a function that makes a Replete instance. A Replete 2 | // instance can be used to evaluate code in a variety of JavaScript runtimes. 3 | 4 | /*jslint node, deno, bun */ 5 | 6 | import child_process from "node:child_process"; 7 | import fs from "node:fs"; 8 | import node_resolve from "./node_resolve.js"; 9 | import make_browser_repl from "./browser_repl.js"; 10 | import make_node_repl from "./node_repl.js"; 11 | import make_deno_repl from "./deno_repl.js"; 12 | import make_tjs_repl from "./tjs_repl.js"; 13 | import make_bun_repl from "./bun_repl.js"; 14 | 15 | function make_replete({ 16 | 17 | // Required parameters. 18 | 19 | on_result, 20 | root_locator, 21 | 22 | // Browser REPL configuration. 23 | 24 | browser_port, 25 | browser_hostname, 26 | browser_padawan_type, 27 | browser_humanoid, 28 | 29 | // Node.js REPL configuration. 30 | 31 | which_node, 32 | node_args, 33 | node_env, 34 | 35 | // Deno REPL configuration. 36 | 37 | which_deno, 38 | deno_args, 39 | deno_env, 40 | 41 | // Txiki REPL configuration. 42 | 43 | which_tjs, 44 | tjs_args, 45 | tjs_env, 46 | 47 | // Bun REPL configuration. 48 | 49 | which_bun, 50 | bun_args, 51 | bun_env, 52 | 53 | // These are the capabilities given to the REPLs. See README.md for an 54 | // explanation of each. 55 | 56 | // The 'source' option has been superseded by the 'command' option, but is 57 | // included for backward compatibility. 58 | 59 | source = function default_source(message) { 60 | return Promise.resolve(message.source); 61 | }, 62 | command = function default_command(message) { 63 | return source(message).then(function (string) { 64 | message.source = string; 65 | return message; 66 | }); 67 | }, 68 | read = function default_read(locator) { 69 | return fs.promises.readFile(new URL(locator)); 70 | }, 71 | mime = function default_mime(locator) { 72 | 73 | // Deprecated. Use 'headers' instead. 74 | 75 | if (locator.endsWith(".js") || locator.endsWith(".mjs")) { 76 | return "text/javascript"; 77 | } 78 | }, 79 | headers = function default_headers(locator) { 80 | 81 | // By default, only JavaScript files are served. If you wish to serve other 82 | // types of files, such as images, just check the file extension return 83 | // suitable headers. 84 | 85 | const type = mime(locator); 86 | if (type !== undefined) { 87 | return {"Content-Type": type}; 88 | } 89 | }, 90 | locate = function default_locate( 91 | specifier, 92 | 93 | // When the parent module's locator is omitted, as is the case when an import 94 | // statement is evaluated from within an unsaved editor buffer, resolving from 95 | // the root is better than nothing. 96 | 97 | parent_locator = root_locator 98 | ) { 99 | 100 | // Fully qualified specifiers, such as HTTP URLs or absolute paths, are left for 101 | // the runtime to resolve. 102 | 103 | if (/^\w+:/.test(specifier)) { 104 | return Promise.resolve(specifier); 105 | } 106 | 107 | // Relative paths are simply adjoined to the parent module's locator. 108 | 109 | if (specifier.startsWith(".") || specifier.startsWith("/")) { 110 | return Promise.resolve(new URL(specifier, parent_locator).href); 111 | } 112 | 113 | // Any other specifier is assumed to designate a file in some "node_modules" 114 | // directory reachable by the parent module. 115 | 116 | // Deno does not expose its machinery for searching "node_modules". 117 | // Node.js does, via 'import.meta.resolve', but in Node.js v20 this function 118 | // became synchronous and thus a performance hazard. 119 | 120 | // So, we do it the hard way. 121 | 122 | return node_resolve(specifier, parent_locator); 123 | }, 124 | watch = function default_watch(locator) { 125 | return new Promise(function (resolve, reject) { 126 | const watcher = fs.watch(new URL(locator), resolve); 127 | watcher.on("error", reject); 128 | watcher.on("change", watcher.close); 129 | }); 130 | }, 131 | out = function default_out(string) { 132 | on_result({out: string}); 133 | }, 134 | err = function default_err(string) { 135 | on_result({err: string}); 136 | }, 137 | spawn = function default_spawn(command, env) { 138 | return child_process.spawn(command[0], command.slice(1), {env}); 139 | } 140 | }) { 141 | 142 | function safe_read(locator) { 143 | 144 | // To avoid inadvertently exposing sensitive files to the network, we refuse to 145 | // read any files outside the 'root_locator'. 146 | 147 | const locator_href = new URL(locator).href; 148 | 149 | // Ensure that the locator points to a file within the root directory. We are 150 | // forced to ignore case due to the case-insensitive nature of Windows drive 151 | // letters. 152 | 153 | if ( 154 | !locator_href.toLowerCase().startsWith( 155 | root_locator.toLowerCase() 156 | ) 157 | ) { 158 | return Promise.reject(new Error( 159 | "Forbidden: " + locator + " is outside " + root_locator 160 | )); 161 | } 162 | return read(locator); 163 | } 164 | 165 | // Configurate a REPL for each platform. 166 | 167 | const capabilities = Object.freeze({ 168 | source, 169 | command, 170 | locate, 171 | read: safe_read, 172 | watch, 173 | headers, 174 | out, 175 | err, 176 | spawn 177 | }); 178 | const repls = Object.create(null); 179 | if (browser_port !== undefined) { 180 | repls.browser = make_browser_repl( 181 | capabilities, 182 | browser_port, 183 | browser_hostname, 184 | browser_padawan_type, 185 | browser_humanoid 186 | ); 187 | } 188 | if (which_node !== undefined) { 189 | repls.node = make_node_repl( 190 | capabilities, 191 | which_node, 192 | node_args, 193 | node_env 194 | ); 195 | } 196 | if (which_deno !== undefined) { 197 | repls.deno = make_deno_repl( 198 | capabilities, 199 | which_deno, 200 | deno_args, 201 | deno_env 202 | ); 203 | } 204 | if (which_bun !== undefined) { 205 | repls.bun = make_bun_repl( 206 | capabilities, 207 | which_bun, 208 | bun_args, 209 | bun_env 210 | ); 211 | } 212 | if (which_tjs !== undefined) { 213 | repls.tjs = make_tjs_repl( 214 | capabilities, 215 | which_tjs, 216 | tjs_args, 217 | tjs_env 218 | ); 219 | } 220 | 221 | function start() { 222 | return Promise.all(Object.values(repls).map(function (repl) { 223 | return repl.start(); 224 | })); 225 | } 226 | 227 | function stop() { 228 | return Promise.all(Object.values(repls).map(function (repl) { 229 | return repl.stop(); 230 | })); 231 | } 232 | 233 | function send(message) { 234 | 235 | // Relay the incoming command message to the relevant REPL. The REPL's 236 | // responses are relayed back as result messages. 237 | 238 | const repl = repls[message.platform]; 239 | if (repl === undefined) { 240 | return Promise.reject(new Error( 241 | "Platform unavailable: " + message.platform + ". " 242 | + "Use the \"" + ( 243 | message.platform === "browser" 244 | ? "browser_port" 245 | : "which_" + message.platform 246 | ) + "\" option." 247 | )); 248 | } 249 | return repl.send(message, function (evaluation, exception) { 250 | 251 | // The browser REPL may yield multiple results for each command, when multiple 252 | // tabs are connected. Only one of 'evaluation' and 'exception' is a string, 253 | // the other is undefined. 254 | 255 | on_result({ 256 | evaluation, 257 | exception, 258 | id: message.id 259 | }); 260 | }); 261 | } 262 | 263 | if (!root_locator.endsWith("/")) { 264 | throw new Error("Missing trailing slash in 'root_locator'."); 265 | } 266 | return Object.freeze({start, send, stop}); 267 | } 268 | 269 | export default Object.freeze(make_replete); 270 | -------------------------------------------------------------------------------- /cmdl.js: -------------------------------------------------------------------------------- 1 | // The CMDL remotely evaluates arbitrary source code in command-line JavaScript 2 | // runtimes. 3 | 4 | // The CMDL is not a security feature. It should not be used to run untrusted 5 | // code. Evaluated code will be able to read and write to disk, access the 6 | // network and start new processes. 7 | 8 | // Code is evaluated in the global context. 9 | // Code is evaluated in sloppy mode (as opposed to strict mode). 10 | // Code is evaluated in a dedicated process. 11 | 12 | // A CMDL instance instructs a single padawan. A padawan is a process that is 13 | // used as an isolated execution environment. If a padawan dies, it is 14 | // resurrected immediately. 15 | 16 | // When a CMDL is created, it waits for a TCP connection to be initiated by the 17 | // padawan. Via this communication channel, the CMDL instructs the padawan to 18 | // evaluate JavaScript source code. 19 | 20 | // The TCP server sends commands and receives reports, each of which is a 21 | // JSON-encoded object followed by a newline. 22 | 23 | // +-------------------------------------+ 24 | // | | 25 | // | Master process | 26 | // | | 27 | // | +-----------------------+ | 28 | // | | | | 29 | // | | CMDL | | 30 | // | | | | 31 | // | +-------+---------------+ | 32 | // | | ^ | 33 | // | | | | 34 | // | command report | 35 | // | | | | 36 | // | V | | 37 | // | +----------------+------+ | 38 | // | | | | 39 | // | | TCP server | | 40 | // | | | | 41 | // | +-------+---------------+ | 42 | // | | ^ | 43 | // | | | | 44 | // +--------------|--------|-------------+ 45 | // | | 46 | // command report 47 | // | | 48 | // V | 49 | // +-----------------------+-------------+ 50 | // | | 51 | // | Padawan process | 52 | // | | 53 | // +-------------------------------------+ 54 | 55 | // There is only one kind of command, and that is the "eval" command. The "eval" 56 | // command is an object containing these properties: 57 | 58 | // script: 59 | // The JavaScript source code to be evaluated. It must not contain any 60 | // import or export statements. 61 | 62 | // imports: 63 | // An array of import specifier strings. These will be resolved before 64 | // the script is evaluated, and an array of the resultant module 65 | // objects will be provided in the '$imports' variable. 66 | 67 | // wait: 68 | // Whether to wait for the evaluated value to resolve, if it is a 69 | // Promise. 70 | 71 | // id: 72 | // A unique identifier for the evaluation. It may be any JSON-encodable 73 | // value. It is used to match reports to commands. 74 | 75 | // After evaluation has completed, successfully or not, a report is sent back to 76 | // the CMDL. A report is an object with the following properties: 77 | 78 | // evaluation: 79 | // A string representation of the evaluated value, if evaluation 80 | // succeeded. 81 | 82 | // exception: 83 | // A string representation of the exception, if evaluation failed. 84 | 85 | // id: 86 | // The ID of the corresponding evaluation. 87 | 88 | import net from "node:net"; 89 | import readline from "node:readline"; 90 | 91 | // This should be "localhost", but we force IPv4 because, on Windows, Node.js 92 | // seems unwilling to connect to Deno over IPv6. 93 | 94 | const tcp_hostname = "127.0.0.1"; 95 | 96 | function make_cmdl(spawn_padawan, on_stdout, on_stderr) { 97 | 98 | // The 'spawn_padawan' parameter is the function responsible for starting a 99 | // padawan process. It is passed the port number of the running TCP server, and 100 | // returns a Promise resolving to the ChildProcess object. It may be called 101 | // more than once, to restart the padawan if it dies. 102 | 103 | // The 'on_stdout' and 'on_stderr' parameters are functions that are called with 104 | // a Buffer whenever data is written to stdout or stderr. 105 | 106 | // The return value is an object with the same interface as a padawan described 107 | // in webl_server.js. 108 | 109 | let padawan_process; 110 | let socket; 111 | let tcp_server = net.createServer(); 112 | let report_callbacks = Object.create(null); 113 | 114 | function wait_for_connection() { 115 | 116 | // The returned Promise resolves once a TCP connection with the padawan has been 117 | // established. 118 | 119 | return new Promise(function (resolve) { 120 | return tcp_server.once("connection", function (the_socket) { 121 | socket = the_socket; 122 | readline.createInterface({input: socket}).on( 123 | "line", 124 | function relay_report(line) { 125 | const report = JSON.parse(line); 126 | const id = report.id; 127 | delete report.id; 128 | return report_callbacks[id](report); 129 | } 130 | ); 131 | return resolve(); 132 | }); 133 | }); 134 | } 135 | 136 | function start_padawan() { 137 | 138 | // Starts the padawan and waits for it to connect to the TCP server. 139 | 140 | function register(the_process) { 141 | the_process.on("exit", function () { 142 | 143 | // Inform any waiting callbacks of the failure. 144 | 145 | Object.values(report_callbacks).forEach(function (callback) { 146 | return callback({exception: "CMDL died."}); 147 | }); 148 | report_callbacks = Object.create(null); 149 | 150 | // If the padawan starts correctly but then dies due to its own actions, it is 151 | // restarted immediately. For example, the padawan may be asked to evaluate 152 | // "process.exit();". In such a case, we get the padawan back on line as soon as 153 | // possible. 154 | 155 | if (!the_process.killed && socket !== undefined) { 156 | start_padawan(); 157 | } 158 | socket = undefined; 159 | }); 160 | padawan_process = the_process; 161 | return new Promise(function (resolve, reject) { 162 | the_process.on("error", reject); 163 | the_process.on("spawn", function () { 164 | the_process.stdout.on("data", on_stdout); 165 | the_process.stderr.on("data", on_stderr); 166 | resolve(the_process); 167 | }); 168 | }); 169 | } 170 | 171 | const tcp_host = tcp_hostname + ":" + tcp_server.address().port; 172 | return Promise.all([ 173 | spawn_padawan(tcp_host).then(register), 174 | wait_for_connection() 175 | ]); 176 | } 177 | 178 | function create() { 179 | if (tcp_server.listening) { 180 | return Promise.resolve(); 181 | } 182 | return new Promise( 183 | function start_tcp_server(resolve, reject) { 184 | tcp_server.on("error", reject); 185 | 186 | // The TCP server is allocated an unused port number by the system. 187 | 188 | return tcp_server.listen(0, tcp_hostname, resolve); 189 | } 190 | ).then( 191 | start_padawan 192 | ); 193 | } 194 | 195 | function eval_module(script, imports, wait) { 196 | const id = String(Math.random()); 197 | return new Promise(function (resolve) { 198 | report_callbacks[id] = resolve; 199 | return socket.write( 200 | JSON.stringify({script, imports, wait, id}) + "\n" 201 | ); 202 | }); 203 | } 204 | 205 | function destroy() { 206 | return new Promise(function (resolve) { 207 | if (padawan_process !== undefined) { 208 | padawan_process.kill(); 209 | } 210 | return tcp_server.close(resolve); 211 | }); 212 | } 213 | 214 | return Object.freeze({ 215 | create, 216 | eval: eval_module, 217 | destroy 218 | }); 219 | } 220 | 221 | export default Object.freeze(make_cmdl); 222 | -------------------------------------------------------------------------------- /plugins/sublime/replete.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import sublime 4 | import sublime_plugin 5 | import subprocess 6 | import queue 7 | import json 8 | import uuid 9 | import pathlib 10 | 11 | running_repls = [] 12 | 13 | def merge_dicts(*dicts): 14 | merged = {} 15 | for the_dict in dicts: 16 | merged.update(the_dict) 17 | return merged 18 | 19 | def load_replete_json(cwd): 20 | if cwd is not None: 21 | try: 22 | with open(os.path.join(cwd, "replete.json")) as file: 23 | return json.load(file) 24 | except: 25 | return {} 26 | return {} 27 | 28 | def load_settings(): 29 | try: 30 | return sublime.load_settings("Replete.sublime-settings").to_dict() 31 | except: 32 | return {} 33 | 34 | class REPL(object): 35 | 36 | # A 'REPL' represents an instance of Replete. Each REPL has a dedicated view 37 | # that holds its textual output. 38 | 39 | def __init__(self, view): 40 | self.view = view 41 | self.id = uuid.uuid4().hex 42 | view.settings().set("repl_id", self.id) 43 | cwd = view.settings().get("repl_cwd") 44 | 45 | # Start the Replete process. STDERR is piped to STDOUT for easy access, and we 46 | # configure the input and output streams to deal in lines of text. 47 | 48 | self.popen = subprocess.Popen( 49 | merge_dicts(load_settings(), load_replete_json(cwd))["command"], 50 | cwd=cwd, 51 | stderr=subprocess.STDOUT, 52 | stdin=subprocess.PIPE, 53 | stdout=subprocess.PIPE, 54 | bufsize=1, # buffer by line 55 | universal_newlines=True, 56 | text=True, 57 | 58 | # Extend the environment with NO_COLOR=1 to prevent Deno coloring its output, 59 | # then merge in any overrides from the settings. 60 | 61 | env=merge_dicts( 62 | os.environ, 63 | {"NO_COLOR": "1"}, 64 | load_settings().get("env", {}), 65 | load_replete_json(cwd).get("env", {}) 66 | ), 67 | creationflags=( 68 | subprocess.CREATE_NO_WINDOW # only on Windows 69 | if hasattr(subprocess, "CREATE_NO_WINDOW") 70 | else 0 71 | ) 72 | ) 73 | 74 | # Result messages are processed as they arrive. We use a thread as a worker, 75 | # which places each line of STDOUT it receives into a queue. We monitor the 76 | # queue via Sublime's event loop. 77 | 78 | results_queue = queue.Queue() 79 | def read_results(): 80 | for line in self.popen.stdout: 81 | results_queue.put(line) 82 | reader = threading.Thread(target=read_results, daemon=True) 83 | reader.start() 84 | def check_queue(): 85 | try: 86 | while True: 87 | line = results_queue.get_nowait() 88 | try: 89 | 90 | # Usually, each line is a JSON-encoded message object. If Replete crashes, 91 | # however, the line may contain arbitrary error information. 92 | 93 | message = json.loads(line) 94 | except: 95 | message = {"err": line} 96 | view.run_command("replete_receive", {"message": message}) 97 | except queue.Empty: 98 | sublime.set_timeout(check_queue, 100) 99 | check_queue() 100 | 101 | def stop(self): 102 | self.popen.terminate() 103 | 104 | def send_command(self, message): 105 | self.popen.stdin.write(json.dumps(message) + "\n") 106 | self.popen.stdin.flush() 107 | 108 | def find_repl(view): 109 | 110 | # The 'find_repl' function returns the REPL associated with the 'view', if any. 111 | 112 | for repl in running_repls: 113 | if repl.id == view.settings().get("repl_id"): 114 | return repl 115 | 116 | def find_repl_view(window): 117 | 118 | # The 'find_repl_view' function returns the view from the 'window' that is 119 | # (or was) associated with a REPL. 120 | 121 | for view in window.views(): 122 | if view.settings().get("repl_id") is not None: 123 | return view 124 | 125 | class RepleteCommand(sublime_plugin.TextCommand): 126 | 127 | # Starts a REPL in the current window. If a REPL already exists, it is 128 | # restarted. 129 | 130 | def run(self, edit): 131 | view = find_repl_view(self.view.window()) 132 | if view is not None: 133 | 134 | # There is already a REPL for in this window. Restart it, keeping its view 135 | # intact. 136 | 137 | repl = find_repl(view) 138 | if repl is not None: 139 | repl.stop() 140 | running_repls.remove(repl) 141 | running_repls.append(REPL(view)) 142 | view.insert(edit, view.size(), "REPL restarted.\n") 143 | return 144 | 145 | # Infer the current working directory from the root folder opened in the current 146 | # window. 147 | 148 | cwd = None 149 | file_name = self.view.file_name() 150 | if file_name is not None: 151 | cwd = os.path.dirname(file_name) 152 | window = self.view.window() 153 | if len(window.folders()) > 0: 154 | cwd = window.folders()[0] 155 | 156 | # Create a new view to hold the REPL's output. 157 | 158 | view = window.new_file() 159 | view.set_scratch(True) 160 | view.set_name("[Replete]") 161 | view.settings().set("repl_cwd", cwd) 162 | view.settings().set("spell_check", False) 163 | view.settings().set("line_numbers", False) 164 | view.settings().set("gutter", False) 165 | 166 | # Start the REPL process and attach it to the view. 167 | 168 | running_repls.append(REPL(view)) 169 | 170 | # Move the REPL view to the next group. 171 | 172 | (group, index) = window.get_view_index(view) 173 | window.set_view_index(view, group + 1, 0) 174 | 175 | # Return focus to the non-REPL view. 176 | 177 | window.focus_view(self.view) 178 | 179 | class RepleteClearCommand(sublime_plugin.TextCommand): 180 | 181 | # Empties the output views of all the REPLs. 182 | 183 | def run(self, edit): 184 | view = find_repl_view(self.view.window()) 185 | if view is not None: 186 | view.run_command("select_all") 187 | view.run_command("right_delete") 188 | 189 | class RepleteSendCommand(sublime_plugin.TextCommand): 190 | 191 | # Sends a command message to Replete. 192 | 193 | def run( 194 | self, 195 | edit, 196 | platform, 197 | color="region.yellowish", 198 | **additional_properties 199 | ): 200 | 201 | # Expand each selected region to fill the line. This is generally more useful 202 | # than evaluating nothing. 203 | 204 | regions = [ 205 | ( 206 | self.view.expand_by_class( 207 | region, 208 | sublime.CLASS_LINE_START | sublime.CLASS_LINE_END 209 | ) 210 | if len(region) == 0 211 | else region 212 | ) 213 | for region 214 | in self.view.sel() 215 | ] 216 | 217 | # Construct a command message containing the selected source code. 218 | 219 | message = { 220 | "source": "\n".join([ 221 | self.view.substr(region) 222 | for region 223 | in regions 224 | ]), 225 | "platform": platform, 226 | "scope": str(self.view.id()) 227 | } 228 | if self.view.file_name() != None: 229 | message["locator"] = pathlib.Path(self.view.file_name()).as_uri() 230 | 231 | # Key bindings specified by the user may augment the message with arbitrary 232 | # properties. Because these properties will be available to Replete's "source" 233 | # capability, they can be used to dictate how the source is interpreted. 234 | 235 | for name, value in additional_properties.items(): 236 | message[name] = value 237 | 238 | # Send the command to the window's REPL, if one can be found. 239 | 240 | view = find_repl_view(self.view.window()) 241 | if view is None: 242 | return 243 | try: 244 | find_repl(view).send_command(message) 245 | except: 246 | view.insert(edit, view.size(), "REPL not running.\n") 247 | view.show(view.size()) 248 | 249 | # Momentarily highlight the evaluated regions in the view. This is a nice touch. 250 | 251 | key = uuid.uuid4().hex 252 | self.view.add_regions(key, regions, color) 253 | sublime.set_timeout( 254 | lambda: self.view.erase_regions(key), 255 | 300 256 | ) 257 | 258 | class RepleteReceiveCommand(sublime_plugin.TextCommand): 259 | 260 | # Displays a result message from Replete. Implemented as a TextCommand because 261 | # we require an 'edit' token to modify the text of the output view. 262 | 263 | def run(self, edit, message): 264 | string = ( 265 | message.get("out") 266 | or message.get("err") 267 | or message.get("evaluation") 268 | or message.get("exception") 269 | ) 270 | 271 | # The following types of messages do supply a trailing newline, so we add one. 272 | 273 | if "evaluation" in message or "exception" in message: 274 | string += "\n" 275 | 276 | # Append the output string to the view, for perusal. 277 | 278 | self.view.insert(edit, self.view.size(), string) 279 | self.view.show(self.view.size()) 280 | 281 | class RepleteViewListener(sublime_plugin.EventListener): 282 | 283 | # When a REPL's view is closed, the corresponding Replete instance is stopped. 284 | 285 | def on_close(self, view): 286 | repl = find_repl(view) 287 | if repl: 288 | repl.stop() 289 | running_repls.remove(repl) 290 | 291 | def plugin_unloaded(): 292 | 293 | # The 'plugin_unloaded' function is called when this file is about to be 294 | # reloaded on-the-fly. Let's not orphan our subprocesses! 295 | 296 | for repl in running_repls: 297 | repl.stop() 298 | -------------------------------------------------------------------------------- /plugins/nrepl/server.js: -------------------------------------------------------------------------------- 1 | // An nREPL server for Replete. See ./README.md for instructions. 2 | 3 | // $ node server.js [port] # using Node.js 4 | // $ deno run -A server.js [port] # using Deno 5 | 6 | import {Buffer} from "node:buffer"; 7 | import child_process from "node:child_process"; 8 | import console from "node:console"; 9 | import fs from "node:fs"; 10 | import process from "node:process"; 11 | import net from "node:net"; 12 | import os from "node:os"; 13 | import readline from "node:readline"; 14 | import url from "node:url"; 15 | import bencode from "./bencode.js"; 16 | 17 | // The official nREPL protocol specification is vague. 18 | 19 | // https://nrepl.org/nrepl/design/overview.html 20 | // https://nrepl.org/nrepl/ops.html 21 | // https://nrepl.org/nrepl/building_servers.html 22 | 23 | // Missing details were obtained by monitoring the communication between various 24 | // clients and a Clojure nREPL server. For CIDER, monitoring can be enabled by 25 | // typing 26 | 27 | // M-x nrepl-toggle-message-logging 28 | 29 | const hostname = "127.0.0.1"; 30 | const default_command = [ 31 | "deno", 32 | "run", 33 | "--allow-all", 34 | "--importmap", 35 | "https://deno.land/x/replete/import_map.json", 36 | "https://deno.land/x/replete/replete.js", 37 | "--browser_port=9325", 38 | "--content_type=js:text/javascript", 39 | "--content_type=mjs:text/javascript", 40 | "--content_type=map:application/json", 41 | "--content_type=css:text/css", 42 | "--content_type=html:text/html; charset=utf-8", 43 | "--content_type=wasm:application/wasm", 44 | "--content_type=woff2:font/woff2", 45 | "--content_type=svg:image/svg+xml", 46 | "--content_type=png:image/png", 47 | "--content_type=webp:image/webp" 48 | ]; 49 | const rx_platform = /\*([a-z]+)\*/; 50 | 51 | function timestamp() { 52 | return new Date().toJSON().replace("T", " ").replace("Z", "000000"); 53 | } 54 | 55 | function start_replete(command) { 56 | return new Promise(function (resolve, reject) { 57 | const the_process = child_process.spawn( 58 | command[0], 59 | command.slice(1), 60 | {stdio: ["pipe", "pipe", "inherit"]} // passthru stderr 61 | ); 62 | the_process.on("error", reject); 63 | the_process.on("spawn", function () { 64 | resolve(the_process); 65 | }); 66 | }); 67 | } 68 | 69 | function start_tcp() { 70 | return new Promise(function (resolve, reject) { 71 | const tcp_server = net.createServer(); 72 | tcp_server.on("error", reject); 73 | return tcp_server.listen( 74 | Number(process.argv[2]) || 0, 75 | hostname, 76 | function callback() { 77 | resolve(tcp_server); 78 | } 79 | ); 80 | }); 81 | } 82 | 83 | Promise.all([ 84 | start_replete(default_command), 85 | start_tcp() 86 | ]).then(function ([replete_process, tcp_server]) { 87 | let sockets = []; 88 | let sessions = Object.create(null); 89 | let requests = Object.create(null); 90 | let buffer = new Uint8Array(); 91 | let platform = "deno"; 92 | const port = tcp_server.address().port; 93 | 94 | function send_command(message) { 95 | replete_process.stdin.write(JSON.stringify(message) + "\n"); 96 | } 97 | 98 | function send_response(message, routing) { 99 | const socket_nr = sessions[routing.session]; 100 | const socket = sockets[socket_nr]; 101 | if (socket !== undefined) { 102 | socket.write(bencode.encode( 103 | Object.assign({}, message, { 104 | id: routing.id, 105 | session: String(routing.session), 106 | "time-stamp": timestamp() 107 | }) 108 | )); 109 | } 110 | } 111 | 112 | function broadcast_response(message) { 113 | const entries = Object.entries(requests); 114 | entries.forEach(function ([id, session]) { 115 | send_response(message, {id, session}); 116 | }); 117 | return entries.length > 0; 118 | } 119 | 120 | function on_request(socket_nr, message) { 121 | if (message.op === "clone") { 122 | const session_nr = Object.keys(sessions).length; 123 | const session = String(session_nr); 124 | sessions[session] = socket_nr; 125 | return send_response( 126 | { 127 | "new-session": session, 128 | status: ["done"] 129 | }, 130 | { 131 | id: message.id, 132 | session 133 | } 134 | ); 135 | } 136 | const routing = { 137 | id: message.id, 138 | session: message.session, 139 | socket: socket_nr 140 | }; 141 | if (message.op === "close") { 142 | send_response({status: ["done"]}, routing); 143 | delete sessions[message.session]; 144 | return; 145 | } 146 | if (message.op === "describe") { 147 | return send_response( 148 | { 149 | aux: {}, 150 | ops: { 151 | clone: {}, 152 | close: {}, 153 | describe: {}, 154 | eval: {}, 155 | "out-subscribe": {} 156 | }, 157 | versions: { 158 | nrepl: { 159 | "version-string": "1.0.0", 160 | major: "1", 161 | minor: "0", 162 | incremental: "0", 163 | qualifier: "" 164 | }, 165 | clojure: {}, // avoid breaking monroe 166 | java: {} // avoid breaking monroe 167 | }, 168 | status: ["done"] 169 | }, 170 | routing 171 | ); 172 | } 173 | if (message.op === "eval") { 174 | const matches = message.code.trim().match(rx_platform); 175 | if (matches) { 176 | platform = matches[1]; 177 | console.error("Set platform: ", platform); 178 | message.code = "navigator.userAgent"; 179 | } 180 | requests[message.id] = message.session; 181 | let locator; 182 | try { 183 | locator = url.pathToFileURL(message.file); 184 | } catch (_) {} 185 | return send_command({ 186 | platform, 187 | source: message.code, 188 | locator, 189 | scope: message.session + ":" + message.file, 190 | id: routing 191 | }); 192 | } 193 | if (message.op === "out-subscribe") { 194 | requests[message.id] = message.session; 195 | return send_response( 196 | {"out-subscribe": message.session, status: ["done"]}, 197 | routing 198 | ); 199 | } 200 | console.error("Op not supported: ", message.op); 201 | } 202 | 203 | // For the benefit of nREPL clients, print the network address of the server to 204 | // stdout and write the port to the .nrepl-port file. 205 | 206 | console.log( 207 | "nREPL server started on port " + port + " on host " + hostname 208 | + " - nrepl://" + hostname + ":" + port 209 | ); 210 | fs.writeFile(".nrepl-port", String(port), function (error) { 211 | if (error) { 212 | console.error("Failed to write .nrepl-port: ", error); 213 | } 214 | }); 215 | 216 | // Listen for TCP connections from nREPL clients. 217 | 218 | tcp_server.on("connection", function (socket) { 219 | const socket_nr = sockets.length; 220 | sockets.push(socket); 221 | socket.on("data", function (chunk) { 222 | buffer = Buffer.concat([buffer, chunk]); 223 | try { 224 | const [value, at] = bencode.decode_from(buffer, 0); 225 | buffer = buffer.slice(at); 226 | on_request(socket_nr, value); 227 | } catch (_) {} 228 | }); 229 | socket.on("error", console.error); 230 | socket.on("close", function () { 231 | console.error(`nREPL #${socket_nr} disconnected.`); 232 | sockets[socket_nr] = undefined; 233 | }); 234 | console.error(`nREPL #${socket_nr} connected.`); 235 | }); 236 | 237 | // Listen for result messages from Replete. 238 | 239 | readline.createInterface( 240 | {input: replete_process.stdout} 241 | ).on("line", function (line) { 242 | const {id, evaluation, exception, out, err} = JSON.parse(line); 243 | const routing = id; 244 | if (evaluation !== undefined) { 245 | send_response({value: evaluation}, routing); 246 | send_response({status: ["done"]}, routing); 247 | delete requests[routing.id]; 248 | } else if (exception !== undefined) { 249 | send_response({ex: exception, status: ["eval-error"]}, routing); 250 | 251 | // For CIDER at least, it is not enough to send an :ex response because it is 252 | // never displayed anywhere. To be sure that the exception is visible, we must 253 | // also send an :err response. 254 | 255 | send_response({err: exception + "\n"}, routing); 256 | send_response({status: ["done"]}, routing); 257 | delete requests[routing.id]; 258 | } else if (out !== undefined) { 259 | if (!broadcast_response({out})) { 260 | console.log(out); 261 | } 262 | } else if (err !== undefined) { 263 | if (!broadcast_response({err})) { 264 | console.log(err); 265 | } 266 | } 267 | }); 268 | replete_process.on("exit", function (exit_code) { 269 | console.error("Replete exited with code " + exit_code + "."); 270 | process.exit(1); 271 | }); 272 | 273 | function exit() { 274 | replete_process.kill(); 275 | process.exit(); 276 | } 277 | 278 | process.on("SIGTERM", exit); 279 | process.on("SIGINT", exit); 280 | if (os.platform() !== "win32") { 281 | process.on("SIGHUP", exit); 282 | } 283 | }); 284 | -------------------------------------------------------------------------------- /webl/websocketify.js: -------------------------------------------------------------------------------- 1 | // A minimal WebSocket server implementation for Node.js. 2 | 3 | /*jslint bitwise */ 4 | 5 | import {Buffer} from "node:buffer"; 6 | import crypto from "node:crypto"; 7 | 8 | function make_frame(opcode, payload) { 9 | 10 | // The 'make_frame' function takes an integer 'opcode' and a 'payload' Buffer, 11 | // and returns a Buffer containing a single WebSocket frame. 12 | 13 | // We set the "fin" bit to true, meaning that this is the only frame in the 14 | // message. 15 | 16 | const zeroth_byte = Buffer.from([0b10000000 | opcode]); 17 | 18 | // The payload length field expands as required. 19 | 20 | let length_bytes; 21 | if (payload.length < 126) { 22 | length_bytes = Buffer.from([payload.length]); 23 | } else if (payload.length < 2 ** 16) { 24 | length_bytes = Buffer.alloc(3); 25 | length_bytes.writeInt8(126, 0); 26 | length_bytes.writeUInt16BE(payload.length, 1); 27 | } else { 28 | length_bytes = Buffer.alloc(9); 29 | length_bytes.writeInt8(127, 0); 30 | 31 | // The specification allows for an exabyte of payload, which seems excessive for 32 | // a browser-based messaging protocol. This implementation produces a 33 | // well-formed length field for payloads up to 9 petabytes, which ought to be 34 | // enough for anybody. 35 | 36 | length_bytes.writeUInt32BE(Math.floor(payload.length / (2 ** 32)), 1); 37 | length_bytes.writeUInt32BE(payload.length % (2 ** 32), 5); 38 | } 39 | return Buffer.concat([zeroth_byte, length_bytes, payload]); 40 | } 41 | 42 | function websocketify( 43 | server, 44 | on_open, 45 | on_receive, 46 | on_close 47 | ) { 48 | 49 | // The 'websocketify' function empowers an HTTP server to send and receive 50 | // WebSocket messages. The following callbacks must be provided: 51 | 52 | // on_open(connection, req) 53 | // Called when a new connection is opened. 54 | 55 | // on_receive(connection, message) 56 | // Called with each incoming message. The 'message' parameter will be a 57 | // string or a Buffer, depending on the message type. 58 | 59 | // on_close(connection, reason) 60 | // Called when an existing connection is closed. A reason might be 61 | // provided. 62 | 63 | // Each callback takes a 'connection' parameter, which is a frozen object 64 | // unique to a particular connection. It contains the following methods: 65 | 66 | // send(message) 67 | // Sends a message. It should be a string or a Buffer. 68 | 69 | // close(reason) 70 | // Closes the connection. The 'reason' parameter will be passed to the 71 | // on_close callback. 72 | 73 | let sockets = Object.create(null); 74 | let next_socket_id = 0; 75 | server.on("upgrade", function (req, socket) { 76 | 77 | // Assign a unique ID to the socket. 78 | 79 | const socket_id = next_socket_id; 80 | sockets[socket_id] = socket; 81 | next_socket_id += 1; 82 | 83 | // Create a public interface for the socket. 84 | 85 | const connection = Object.freeze({ 86 | send(payload) { 87 | socket.write( 88 | typeof payload === "string" 89 | ? make_frame(0x1, Buffer.from(payload)) 90 | : make_frame(0x2, payload) 91 | ); 92 | }, 93 | close() { 94 | socket.destroy(); 95 | } 96 | }); 97 | 98 | // The WebSocket protocol requires that we compute the hash of a nonce provided 99 | // by the request. This is stupid, why can't we just use the value of the nonce 100 | // directly? 101 | 102 | const sha1 = crypto.createHash("sha1"); 103 | const stupid_protocol_key = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 104 | sha1.update(req.headers["sec-websocket-key"] + stupid_protocol_key); 105 | socket.write([ 106 | "HTTP/1.1 101 Web Socket Protocol Handshake", 107 | "Upgrade: WebSocket", 108 | "Connection: Upgrade", 109 | "Sec-WebSocket-Accept: " + sha1.digest("base64"), 110 | "\r\n" 111 | ].join("\r\n")); 112 | 113 | // The 'buffer' variable holds the bytes that have arrived over the wire, but 114 | // have not yet been consumed. The 'payload_fragment' variable contains the 115 | // accumulated bytes of a message that has been split into multiple frames. The 116 | // 'textual' variable is true if 'payload_fragment' is to be interpreted as 117 | // text. 118 | 119 | let buffer = Buffer.alloc(0); 120 | let payload_fragment; 121 | let textual; 122 | 123 | function consume_buffer() { 124 | 125 | // The 'consume_buffer' function attempts to tease WebSocket frames out of the 126 | // buffer, and WebSocket messages out of frames. 127 | 128 | // This is the binary format of a frame: 129 | 130 | // 0 1 2 3 131 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 132 | // +-+-+-+-+-------+-+-------------+-------------------------------+ 133 | // |F|R|R|R| opcode|M| Payload len | Extended payload length | 134 | // |I|S|S|S| (4) |A| (7) | (16/64) | 135 | // |N|V|V|V| |S| | (if payload len==126/127) | 136 | // | |1|2|3| |K| | | 137 | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 138 | // | Extended payload length continued, if payload len == 127 | 139 | // + - - - - - - - - - - - - - - - +-------------------------------+ 140 | // | |Masking-key, if MASK set to 1 | 141 | // +-------------------------------+-------------------------------+ 142 | // | Masking-key (continued) | Payload Data | 143 | // +-------------------------------- - - - - - - - - - - - - - - - + 144 | // : Payload Data continued ... : 145 | // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 146 | // | Payload Data continued ... | 147 | // +---------------------------------------------------------------+ 148 | 149 | // The integers are big-endian, meaning that the most significant byte arrives 150 | // first. The full WebSockets specification can be found at 151 | // https://datatracker.ietf.org/doc/html/rfc6455. 152 | 153 | if (buffer.length < 2) { 154 | return; 155 | } 156 | const fin = Boolean(buffer[0] >> 7); 157 | const opcode = buffer[0] & 0b1111; 158 | 159 | // Read the payload length field, which can vary in size. 160 | 161 | let payload_length = buffer[1] & 0b01111111; 162 | let mask_start = 2; 163 | if (payload_length === 126) { 164 | mask_start += 2; 165 | if (buffer.length < mask_start) { 166 | return; 167 | } 168 | payload_length = buffer.readUInt16BE(2); 169 | } else if (payload_length > 126) { 170 | mask_start += 8; 171 | if (buffer.length < mask_start) { 172 | return; 173 | } 174 | payload_length = Number(buffer.readBigUInt64BE(2)); 175 | } 176 | 177 | // Proceed only if the buffer contains the entire frame. If it does not, we wait 178 | // for more bytes. 179 | 180 | const mask_length = 4; 181 | if (buffer.length - mask_start < mask_length + payload_length) { 182 | return; 183 | } 184 | 185 | // Consume the payload length. 186 | 187 | buffer = buffer.slice(mask_start); 188 | 189 | // Read and consume the mask. Messages from the client are always masked, 190 | // whereas messages from the server are never masked. 191 | 192 | const mask = buffer.slice(0, mask_length); 193 | buffer = buffer.slice(mask_length); 194 | 195 | // Read and consume the payload. 196 | 197 | const payload = buffer.slice(0, payload_length).map( 198 | function unmask(byte, byte_nr) { 199 | return byte ^ mask[byte_nr % 4]; 200 | } 201 | ); 202 | buffer = buffer.slice(payload_length); 203 | 204 | // The opcode has 16 possible values: 205 | // 0x0 denotes a continuation frame 206 | // 0x1 denotes a text frame 207 | // 0x2 denotes a binary frame 208 | // 0x3-7 are reserved for further non-control frames 209 | // 0x8 denotes a connection close 210 | // 0x9 denotes a ping 211 | // 0xA denotes a pong 212 | // 0xB-F are reserved for further control frames 213 | 214 | if (opcode === 0x1 || opcode === 0x2) { 215 | textual = (opcode === 0x1); 216 | if (fin) { 217 | on_receive( 218 | connection, 219 | ( 220 | textual 221 | ? payload.toString() 222 | : payload 223 | ) 224 | ); 225 | } else { 226 | payload_fragment = payload; 227 | } 228 | } else if (opcode === 0x0) { 229 | payload_fragment = Buffer.concat([payload_fragment, payload]); 230 | if (fin) { 231 | on_receive( 232 | connection, 233 | ( 234 | textual 235 | ? payload_fragment.toString() 236 | : payload_fragment 237 | ) 238 | ); 239 | } 240 | } else if (opcode === 0x9) { 241 | 242 | // Reply to a ping with a pong. The pong must contain the same payload as the 243 | // ping that triggered it. 244 | 245 | socket.write(make_frame(0xA, payload)); 246 | } else { 247 | 248 | // Any other opcode closes the connection. This includes the "close" opcode. 249 | 250 | socket.destroy(); 251 | } 252 | 253 | // Consume any frames left in the buffer. 254 | 255 | return consume_buffer(); 256 | } 257 | 258 | socket.on("data", function (chunk) { 259 | buffer = Buffer.concat([buffer, chunk]); 260 | return consume_buffer(); 261 | }); 262 | 263 | // The "close" event always follows an "error" event. 264 | 265 | let close_reason; 266 | socket.on("error", function (error) { 267 | close_reason = error; 268 | }); 269 | socket.on("close", function () { 270 | delete sockets[socket_id]; 271 | on_close(connection, close_reason); 272 | }); 273 | return on_open(connection, req); 274 | }); 275 | } 276 | 277 | export default Object.freeze(websocketify); 278 | -------------------------------------------------------------------------------- /webl/webl_server.js: -------------------------------------------------------------------------------- 1 | // The WEBL server runs on Node.js, serving the WEBL client to the browser. A 2 | // persistent connection is maintained between the two. The server can be 3 | // started, stopped or asked to make new padawans. 4 | 5 | /*jslint node */ 6 | 7 | import fs from "node:fs"; 8 | import http from "node:http"; 9 | import fileify from "../fileify.js"; 10 | import websocketify from "./websocketify.js"; 11 | const r2d2_svg_url = new URL("./r2d2.svg", import.meta.url); 12 | const c3po_svg_url = new URL("./c3po.svg", import.meta.url); 13 | const webl_js_url = new URL("./webl.js", import.meta.url); 14 | const webl_client_js_url = new URL("./webl_client.js", import.meta.url); 15 | const webl_inspect_js_url = new URL("./webl_inspect.js", import.meta.url); 16 | const webl_relay_js_url = new URL("./webl_relay.js", import.meta.url); 17 | 18 | // There is at least one Firefox bug that is resolved by including a trailing 19 | // newline in the HTML source. 20 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=1880710. 21 | 22 | const html = ` 23 | 24 | 25 | 26 | 27 | `; 28 | 29 | function make_webl_server( 30 | on_exception, 31 | on_client_found, 32 | on_client_lost, 33 | on_unhandled_request = function not_found(_, res) { 34 | res.statusCode = 404; 35 | return res.end(); 36 | }, 37 | humanoid = false 38 | ) { 39 | 40 | // The 'on_exception' parameter is a function that is called when the WEBL 41 | // server itself encounters a problem. 42 | 43 | // When the WEBL server receives an unrecognized HTTP request, it invokes the 44 | // optional 'on_unhandled_request' parameter with the req and res objects. 45 | 46 | // The 'humainoid' parameter determines the WEBL client's favicon. If true, it 47 | // will be the visage of C3PO, otherwise it will be R2D2. 48 | 49 | // The other two parameters are covered below. The constructor returns an object 50 | // containing two functions: 51 | 52 | // start(port, hostname) 53 | // The 'start' method starts the server on the specified 'port' and 54 | // 'hostname', returning a Promise that resolves to the chosen port number 55 | // once the server is ready. The 'port' parameter determines the port of 56 | // the web server. If a port is not specified, one is chosen automatically. 57 | // The 'hostname' parameter defaults to "localhost" if it is undefined. 58 | 59 | // stop() 60 | // The 'stop' method closes down the server. It returns a Promise that 61 | // resolves once the server is stopped. 62 | 63 | // When a WEBL client connects to the server, the 'on_client_found' function is 64 | // called with an interface for the client. Likewise, when a client disconnects, 65 | // the 'on_client_lost' function is called with the same interface object. 66 | 67 | // A client's interface is an object containing two properties: 68 | 69 | // padawan(spec) 70 | // The 'padawan' method returns an interface for a new, unique padawan. It 71 | // takes a 'spec' object, described in ./webl.js. The returned object 72 | // contains three functions: 73 | 74 | // create() 75 | // See ./webl.js. 76 | 77 | // eval(script, imports, wait) 78 | // See ./webl.js. 79 | 80 | // destroy() 81 | // Similar to the function as described in ./webl.js, except that 82 | // it returns a Promise that resolves once the padawan has ceased 83 | // to exist. 84 | 85 | // origin 86 | // The client's location.origin value. 87 | 88 | let sockets = []; 89 | let clients = new WeakMap(); 90 | let on_response_callbacks = Object.create(null); 91 | let on_status_callbacks = Object.create(null); 92 | let padawan_count = 0; 93 | 94 | function make_client(connection, origin) { 95 | 96 | function request(name, parameters) { 97 | 98 | // The 'request' function sends a request message thru the WebSocket connection 99 | // to the client. It returns a Promise that resolves to the value of the 100 | // response. 101 | 102 | const id = String(Math.random()); 103 | connection.send(JSON.stringify({ 104 | type: "request", 105 | id, 106 | name, 107 | parameters 108 | })); 109 | return new Promise(function (resolve, reject) { 110 | on_response_callbacks[id] = function (value, reason) { 111 | return ( 112 | value === undefined 113 | ? reject(reason) 114 | : resolve(value) 115 | ); 116 | }; 117 | }); 118 | } 119 | 120 | function padawan(spec) { 121 | 122 | // Each padawan is assigned a unique name, so that messages originating from 123 | // different padawans may be distinguished. 124 | 125 | let name = "Padawan " + padawan_count; 126 | padawan_count += 1; 127 | 128 | function create() { 129 | on_status_callbacks[name] = function ( 130 | message_name, 131 | parameters 132 | ) { 133 | return ( 134 | message_name === "log" 135 | ? spec.on_log(...parameters.values) 136 | : spec.on_exception(parameters.reason) 137 | ); 138 | }; 139 | return request( 140 | "create_padawan", 141 | { 142 | name, 143 | type: spec.type, 144 | popup_window_features: spec.popup_window_features, 145 | iframe_style_object: spec.iframe_style_object, 146 | iframe_sandbox: spec.iframe_sandbox 147 | } 148 | ); 149 | } 150 | 151 | function eval_module(script, imports, wait) { 152 | return request( 153 | "eval_module", 154 | { 155 | script, 156 | imports, 157 | wait, 158 | padawan_name: name 159 | } 160 | ); 161 | } 162 | 163 | function destroy() { 164 | delete on_status_callbacks[name]; 165 | return request("destroy_padawan", {name}); 166 | } 167 | 168 | return Object.freeze({ 169 | create, 170 | eval: eval_module, 171 | destroy 172 | }); 173 | } 174 | return Object.freeze({padawan, origin}); 175 | } 176 | 177 | const server = http.createServer(function on_request(req, res) { 178 | 179 | function serve_file(url, mime_type) { 180 | return fileify(url).then( 181 | fs.promises.readFile 182 | ).then(function (buffer) { 183 | res.setHeader("Content-Type", mime_type); 184 | return res.end(buffer); 185 | }).catch(function (error) { 186 | on_exception(error); 187 | res.statusCode = 500; 188 | return res.end(); 189 | }); 190 | } 191 | 192 | if (req.url === "/favicon.ico") { 193 | return serve_file( 194 | ( 195 | humanoid 196 | ? c3po_svg_url 197 | : r2d2_svg_url 198 | ), 199 | "image/svg+xml" 200 | ); 201 | } 202 | if (req.url === "/webl.js") { 203 | return serve_file(webl_js_url, "text/javascript"); 204 | } 205 | if (req.url === "/webl_client.js") { 206 | return serve_file(webl_client_js_url, "text/javascript"); 207 | } 208 | if (req.url === "/webl_inspect.js") { 209 | return serve_file(webl_inspect_js_url, "text/javascript"); 210 | } 211 | if (req.url === "/webl_relay.js") { 212 | return serve_file(webl_relay_js_url, "text/javascript"); 213 | } 214 | if (req.url === "/") { 215 | 216 | // Although this HTML source is ASCII-only, its charset will be used as a 217 | // fallback for other files, such as CSS, so we had better specify UTF-8. 218 | 219 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 220 | return res.end(html); 221 | } 222 | return on_unhandled_request(req, res); 223 | }); 224 | 225 | // Modify the server to accept WebSocket connections, in addition to HTTP 226 | // requests. 227 | 228 | websocketify( 229 | server, 230 | function on_open() { 231 | return; 232 | }, 233 | function on_receive(connection, message) { 234 | message = JSON.parse(message); 235 | if (message.type === "ready") { 236 | 237 | // The client is ready to start receiving messages. 238 | 239 | const client = make_client(connection, message.value); 240 | clients.set(connection, client); 241 | on_client_found(client); 242 | } else if (message.type === "response") { 243 | 244 | // Attempt to match up the response with its request, using the request's ID. 245 | 246 | const on_response = on_response_callbacks[message.request_id]; 247 | if (on_response !== undefined) { 248 | return on_response(message.value, message.reason); 249 | } 250 | } else if (message.type === "status") { 251 | 252 | // Attempt to match up status messages with the relevant padawan. 253 | 254 | const status_callback = on_status_callbacks[ 255 | message.value.padawan_name 256 | ]; 257 | if (status_callback !== undefined) { 258 | return status_callback(message.name, message.value); 259 | } 260 | } 261 | }, 262 | function on_close(connection) { 263 | const client = clients.get(connection); 264 | if (client !== undefined) { 265 | clients.delete(connection); 266 | on_client_lost(client); 267 | } 268 | } 269 | ); 270 | 271 | // Keep track of each created socket so they can all be destroyed at once. This 272 | // includes HTTP sockets in addition to WebSockets. 273 | 274 | server.on("connection", function (socket) { 275 | sockets.push(socket); 276 | }); 277 | 278 | function start(port, hostname = "localhost") { 279 | return new Promise(function (resolve, reject) { 280 | server.once("error", reject); 281 | server.listen(port, hostname, function on_ready() { 282 | return resolve(server.address().port); 283 | }); 284 | }); 285 | } 286 | 287 | function stop() { 288 | 289 | // The server will only close down once it no longer has active connections. 290 | // Destroy all open sockets. 291 | 292 | sockets.forEach(function (socket) { 293 | socket.destroy(); 294 | }); 295 | return new Promise(function (resolve) { 296 | return server.close(resolve); 297 | }); 298 | } 299 | 300 | return Object.freeze({start, stop}); 301 | } 302 | 303 | export default Object.freeze(make_webl_server); 304 | -------------------------------------------------------------------------------- /plugins/neovim/lua/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /node_resolve.js: -------------------------------------------------------------------------------- 1 | // Attempts to resolves an import specifier to a file in some "node_modules" 2 | // directory, or to a Node.js builtin like "fs". 3 | 4 | /*jslint node */ 5 | 6 | import console from "node:console"; 7 | import fs from "node:fs"; 8 | import os from "node:os"; 9 | import path from "node:path"; 10 | import url from "node:url"; 11 | 12 | const node_builtin_modules = [ 13 | "assert", "async_hooks", "buffer", "child_process", "cluster", "console", 14 | "constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain", 15 | "events", "fs", "http", "http2", "https", "inspector", "module", "net", 16 | "os", "path", "perf_hooks", "process", "punycode", "querystring", 17 | "readline", "repl", "stream", "string_decoder", "timers", "tls", 18 | "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", 19 | "zlib" 20 | ]; 21 | 22 | function unwrap_export(value) { 23 | 24 | // A conditional export is an object whose properties are branches. A property 25 | // value can be yet another conditional. This function unwraps any nested 26 | // conditionals, returning a string. 27 | 28 | if (typeof value === "string") { 29 | return value; 30 | } 31 | if (Array.isArray(value)) { 32 | return unwrap_export(value[0]); 33 | } 34 | if (value) { 35 | return unwrap_export(value.import || value.module || value.default); 36 | } 37 | } 38 | 39 | function glob_map(string, mappings) { 40 | 41 | // Match a string against an object of glob-style mappings. If a match is found, 42 | // the transformed string is returned. Otherwise the return value is undefined. 43 | 44 | // For example, the mappings 45 | 46 | // { 47 | // "./*.js": "./dist/*.mjs", 48 | // "./assets/*": "./dist/assets/*" 49 | // } 50 | 51 | // will transform the string "./apple/orange.js" into "./dist/apple/orange.mjs", 52 | // and "./assets/image.png" into "./dist/assets/image.png". 53 | 54 | let result; 55 | if (Object.entries(mappings).some(function ([from, to]) { 56 | const [from_prefix, from_suffix] = from.split("*"); 57 | if ( 58 | from_suffix !== undefined 59 | && string.startsWith(from_prefix) 60 | && string.endsWith(from_suffix) 61 | ) { 62 | const filling = string.slice(from_prefix.length, ( 63 | from_suffix.length > 0 64 | ? -from_suffix.length 65 | : undefined 66 | )); 67 | const [to_prefix, to_suffix] = to.split("*"); 68 | if (to_suffix !== undefined) { 69 | result = to_prefix + filling + to_suffix; 70 | return true; 71 | } 72 | } 73 | return false; 74 | })) { 75 | return result; 76 | } 77 | } 78 | 79 | function internalize(external, manifest) { 80 | 81 | // Given a parsed package.json object and a file's external relative path 82 | // (which may be "."), return the file's actual path relative to the 83 | // package.json, or undefined if the external path can not be resolved. 84 | 85 | // The resolution algorithm is based on the loose specification described by 86 | // nodejs.org/api/packages.html and webpack.js.org/guides/package-exports. 87 | 88 | const {exports, main, module} = manifest; 89 | if (exports !== undefined) { 90 | return ( 91 | external === "." 92 | ? unwrap_export(exports["."] ?? exports) 93 | : unwrap_export(exports[external]) ?? glob_map(external, exports) 94 | ); 95 | } 96 | return ( 97 | external === "." 98 | ? module ?? main ?? "./index.js" 99 | : external 100 | ); 101 | } 102 | 103 | function find_manifest(package_name, from_url) { 104 | const manifest_url = new URL( 105 | "node_modules/" + package_name + "/package.json", 106 | from_url 107 | ); 108 | return fs.promises.readFile(manifest_url, "utf8").then(function (json) { 109 | return [JSON.parse(json), manifest_url]; 110 | }).catch(function () { 111 | 112 | // The manifest could not be read. Try searching the parent directory, unless we 113 | // are at the root of the filesystem. 114 | 115 | const parent_url = new URL("../", from_url); 116 | return ( 117 | parent_url.href === from_url.href 118 | ? Promise.resolve([]) 119 | : find_manifest(package_name, parent_url) 120 | ); 121 | }); 122 | } 123 | 124 | function node_resolve(specifier, parent_locator) { 125 | 126 | // If the specifier is a Node.js builtin, simply qualify it as such. 127 | 128 | if (node_builtin_modules.includes(specifier)) { 129 | return Promise.resolve("node:" + specifier); 130 | } 131 | 132 | // Parse the specifier. 133 | 134 | const parts = specifier.split("/"); 135 | const package_name = ( 136 | parts[0].startsWith("@") 137 | ? parts[0] + "/" + parts[1] 138 | : parts[0] 139 | ); 140 | const external = "." + specifier.replace(package_name, ""); 141 | 142 | // Find the package's package.json. 143 | 144 | function fail(message) { 145 | return Promise.reject(new Error( 146 | "Failed to resolve '" + specifier + "' from " 147 | + parent_locator + ". " + message 148 | )); 149 | } 150 | 151 | return find_manifest( 152 | package_name, 153 | new URL(parent_locator) 154 | ).then(function ([manifest, manifest_url]) { 155 | if (manifest === undefined) { 156 | return fail("Package '" + package_name + "' not found."); 157 | } 158 | const internal = internalize(external, manifest); 159 | if (internal === undefined) { 160 | return fail("Not exported."); 161 | } 162 | 163 | // Join the internal path to the manifest URL to to get the file's URL. 164 | 165 | const file_url = new URL(internal, manifest_url); 166 | 167 | // A given module should be instantiated at most once, so it is important to 168 | // ensure that the file URL is canonical. To this aim, we attempt to resolve 169 | // the file's "real" URL by following any symlinks. 170 | 171 | return fs.promises.realpath( 172 | file_url 173 | ).then(function (real_path) { 174 | return url.pathToFileURL(real_path).href; 175 | }).catch(function () { 176 | return file_url.href; 177 | }); 178 | }); 179 | } 180 | 181 | if (import.meta.main) { 182 | const files = { 183 | "a/node_modules/main/package.json": JSON.stringify({ 184 | main: "./main.js" 185 | }), 186 | "a/node_modules/mod/package.json": JSON.stringify({ 187 | main: "./main.js", 188 | module: "./module.js" 189 | }), 190 | "a/node_modules/@scoped/pkg/package.json": JSON.stringify({ 191 | exports: { 192 | ".": "./scoped.js", 193 | "./exported.js": "./dist/exported.js" 194 | } 195 | }), 196 | "a/node_modules/exports/package.json": JSON.stringify({ 197 | main: "./main.js", 198 | module: "./module.js", 199 | exports: { 200 | ".": { 201 | types: "./dist/types.d.ts", 202 | import: { 203 | node: "./dist/import_node.mjs", 204 | default: "./dist/import_default.js" 205 | }, 206 | require: "./dist/require.js" 207 | }, 208 | "./default.js": { 209 | require: "./dist/default.cjs", 210 | default: "./dist/default.mjs" 211 | }, 212 | "./extensionless": "./dist/extensioned.js", 213 | "./wildcard/*": "./dist/wildcard/*", 214 | "./wildcard_ext/*.js": "./dist/wildcard_ext/*.js", 215 | "./asset.svg": "./dist/asset.svg" 216 | } 217 | }), 218 | "a/b/c/node_modules/nested/package.json": JSON.stringify({ 219 | exports: "./nested.js" 220 | }) 221 | }; 222 | const tests = [ 223 | { 224 | specifier: "exports", 225 | parent: "a/b.js", 226 | resolved: "a/node_modules/exports/dist/import_default.js" 227 | }, 228 | { 229 | specifier: "exports/default.js", 230 | parent: "a/b.js", 231 | resolved: "a/node_modules/exports/dist/default.mjs" 232 | }, 233 | { 234 | specifier: "exports/extensionless", 235 | parent: "a/b.js", 236 | resolved: "a/node_modules/exports/dist/extensioned.js" 237 | }, 238 | { 239 | specifier: "exports/asset.svg", 240 | parent: "a/b.js", 241 | resolved: "a/node_modules/exports/dist/asset.svg" 242 | }, 243 | { 244 | specifier: "exports/wildcard/img.svg", 245 | parent: "a/b.js", 246 | resolved: "a/node_modules/exports/dist/wildcard/img.svg" 247 | }, 248 | { 249 | specifier: "exports/wildcard_ext/hello.js", 250 | parent: "a/b.js", 251 | resolved: "a/node_modules/exports/dist/wildcard_ext/hello.js" 252 | }, 253 | { 254 | specifier: "exports/wildcard_ext/img.wrongext", 255 | parent: "a/b.js" 256 | }, 257 | { 258 | specifier: "exports/internal.js", 259 | parent: "a/b.js" 260 | }, 261 | { 262 | specifier: "main", 263 | parent: "a/b/c/d.js", 264 | resolved: "a/node_modules/main/main.js" 265 | }, 266 | { 267 | specifier: "main/internal.js", 268 | parent: "a/b/c/d.js", 269 | resolved: "a/node_modules/main/internal.js" 270 | }, 271 | { 272 | specifier: "mod", 273 | parent: "a/b.js", 274 | resolved: "a/node_modules/mod/module.js" 275 | }, 276 | { 277 | specifier: "@scoped/pkg", 278 | parent: "a/b.js", 279 | resolved: "a/node_modules/@scoped/pkg/scoped.js" 280 | }, 281 | { 282 | specifier: "@scoped/pkg/exported.js", 283 | parent: "a/b.js", 284 | resolved: "a/node_modules/@scoped/pkg/dist/exported.js" 285 | }, 286 | { 287 | specifier: "nested", 288 | parent: "a/b.js" 289 | }, 290 | { 291 | specifier: "nested", 292 | parent: "a/b/c/d.js", 293 | resolved: "a/b/c/node_modules/nested/nested.js" 294 | }, 295 | { 296 | specifier: "not_found", 297 | parent: "a/b.js" 298 | } 299 | ]; 300 | fs.promises.mkdtemp( 301 | path.join(os.tmpdir(), "node_resolve_") 302 | ).then(function (tmp) { 303 | return Promise.all( 304 | Object.keys(files).map(path.dirname).map(function (directory) { 305 | return fs.promises.mkdir( 306 | path.join(tmp, directory), 307 | {recursive: true} 308 | ); 309 | }) 310 | ).then(function () { 311 | return Promise.all(Object.entries( 312 | files 313 | ).map(function ([file, content]) { 314 | return fs.promises.writeFile(path.join(tmp, file), content); 315 | })); 316 | }).then(function () { 317 | return Promise.all( 318 | tests.map(function ({specifier, parent, resolved}) { 319 | return node_resolve( 320 | specifier, 321 | url.pathToFileURL(path.join(tmp, parent)).href 322 | ).then(function (actual) { 323 | const expect = url.pathToFileURL( 324 | path.join(tmp, resolved) 325 | ).href; 326 | if (actual !== expect) { 327 | return Promise.reject({ 328 | specifier, 329 | parent, 330 | resolved, 331 | actual 332 | }); 333 | } 334 | }).catch(function (error) { 335 | if (resolved !== undefined) { 336 | return Promise.reject(error); 337 | } 338 | }); 339 | }) 340 | ); 341 | }); 342 | }).then(function () { 343 | console.log("All tests passed. You are awesome!"); 344 | }); 345 | } 346 | 347 | export default Object.freeze(node_resolve); 348 | --------------------------------------------------------------------------------