summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Cargo.toml3
-rw-r--r--cli/args/flags.rs117
-rw-r--r--cli/main.rs3
-rw-r--r--cli/module_loader.rs5
-rw-r--r--cli/tests/testdata/jupyter/integration_test.ipynb620
-rw-r--r--cli/tools/jupyter/install.rs95
-rw-r--r--cli/tools/jupyter/jupyter_msg.rs268
-rw-r--r--cli/tools/jupyter/mod.rs139
-rw-r--r--cli/tools/jupyter/resources/deno-logo-32x32.pngbin0 -> 1029 bytes
-rw-r--r--cli/tools/jupyter/resources/deno-logo-64x64.pngbin0 -> 2066 bytes
-rw-r--r--cli/tools/jupyter/server.rs724
-rw-r--r--cli/tools/mod.rs1
-rw-r--r--cli/tools/repl/mod.rs7
-rw-r--r--cli/tools/repl/session.rs63
14 files changed, 2019 insertions, 26 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 8906488e9..58a45538a 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -65,12 +65,14 @@ async-trait.workspace = true
base32 = "=0.4.0"
base64.workspace = true
bincode = "=1.3.3"
+bytes.workspace = true
cache_control.workspace = true
chrono.workspace = true
clap = { version = "=4.3.3", features = ["string"] }
clap_complete = "=4.3.1"
clap_complete_fig = "=4.3.1"
console_static_text.workspace = true
+data-encoding.workspace = true
data-url.workspace = true
dissimilar = "=1.0.4"
dprint-plugin-json = "=0.17.4"
@@ -120,6 +122,7 @@ twox-hash = "=1.6.3"
typed-arena = "=2.0.1"
uuid = { workspace = true, features = ["serde"] }
walkdir = "=2.3.2"
+zeromq = { version = "=0.3.3", default-features = false, features = ["tcp-transport", "tokio-runtime"] }
zstd.workspace = true
[target.'cfg(windows)'.dependencies]
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index b69f1ce8f..40aa7b8e3 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -159,6 +159,13 @@ pub struct InstallFlags {
}
#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct JupyterFlags {
+ pub install: bool,
+ pub kernel: bool,
+ pub conn_file: Option<PathBuf>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UninstallFlags {
pub name: String,
pub root: Option<PathBuf>,
@@ -276,6 +283,7 @@ pub enum DenoSubcommand {
Init(InitFlags),
Info(InfoFlags),
Install(InstallFlags),
+ Jupyter(JupyterFlags),
Uninstall(UninstallFlags),
Lsp,
Lint(LintFlags),
@@ -678,7 +686,8 @@ impl Flags {
std::env::current_dir().ok()
}
Bundle(_) | Completions(_) | Doc(_) | Fmt(_) | Init(_) | Install(_)
- | Uninstall(_) | Lsp | Lint(_) | Types | Upgrade(_) | Vendor(_) => None,
+ | Uninstall(_) | Jupyter(_) | Lsp | Lint(_) | Types | Upgrade(_)
+ | Vendor(_) => None,
}
}
@@ -818,6 +827,7 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::error::Result<Flags> {
"init" => init_parse(&mut flags, &mut m),
"info" => info_parse(&mut flags, &mut m),
"install" => install_parse(&mut flags, &mut m),
+ "jupyter" => jupyter_parse(&mut flags, &mut m),
"lint" => lint_parse(&mut flags, &mut m),
"lsp" => lsp_parse(&mut flags, &mut m),
"repl" => repl_parse(&mut flags, &mut m),
@@ -919,6 +929,7 @@ fn clap_root() -> Command {
.subcommand(init_subcommand())
.subcommand(info_subcommand())
.subcommand(install_subcommand())
+ .subcommand(jupyter_subcommand())
.subcommand(uninstall_subcommand())
.subcommand(lsp_subcommand())
.subcommand(lint_subcommand())
@@ -1613,6 +1624,33 @@ These must be added to the path manually if required.")
)
}
+fn jupyter_subcommand() -> Command {
+ Command::new("jupyter")
+ .arg(
+ Arg::new("install")
+ .long("install")
+ .help("Installs kernelspec, requires 'jupyter' command to be available.")
+ .conflicts_with("kernel")
+ .action(ArgAction::SetTrue)
+ )
+ .arg(
+ Arg::new("kernel")
+ .long("kernel")
+ .help("Start the kernel")
+ .conflicts_with("install")
+ .requires("conn")
+ .action(ArgAction::SetTrue)
+ )
+ .arg(
+ Arg::new("conn")
+ .long("conn")
+ .help("Path to JSON file describing connection parameters, provided by Jupyter")
+ .value_parser(value_parser!(PathBuf))
+ .value_hint(ValueHint::FilePath)
+ .conflicts_with("install"))
+ .about("Deno kernel for Jupyter notebooks")
+}
+
fn uninstall_subcommand() -> Command {
Command::new("uninstall")
.about("Uninstall a script previously installed with deno install")
@@ -3166,6 +3204,18 @@ fn install_parse(flags: &mut Flags, matches: &mut ArgMatches) {
});
}
+fn jupyter_parse(flags: &mut Flags, matches: &mut ArgMatches) {
+ let conn_file = matches.remove_one::<PathBuf>("conn");
+ let kernel = matches.get_flag("kernel");
+ let install = matches.get_flag("install");
+
+ flags.subcommand = DenoSubcommand::Jupyter(JupyterFlags {
+ install,
+ kernel,
+ conn_file,
+ });
+}
+
fn uninstall_parse(flags: &mut Flags, matches: &mut ArgMatches) {
let root = matches.remove_one::<PathBuf>("root");
@@ -7829,4 +7879,69 @@ mod tests {
}
);
}
+
+ #[test]
+ fn jupyter() {
+ let r = flags_from_vec(svec!["deno", "jupyter", "--unstable"]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Jupyter(JupyterFlags {
+ install: false,
+ kernel: false,
+ conn_file: None,
+ }),
+ unstable: true,
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec!["deno", "jupyter", "--unstable", "--install"]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Jupyter(JupyterFlags {
+ install: true,
+ kernel: false,
+ conn_file: None,
+ }),
+ unstable: true,
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "jupyter",
+ "--unstable",
+ "--kernel",
+ "--conn",
+ "path/to/conn/file"
+ ]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Jupyter(JupyterFlags {
+ install: false,
+ kernel: true,
+ conn_file: Some(PathBuf::from("path/to/conn/file")),
+ }),
+ unstable: true,
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "jupyter",
+ "--install",
+ "--conn",
+ "path/to/conn/file"
+ ]);
+ r.unwrap_err();
+ let r = flags_from_vec(svec!["deno", "jupyter", "--kernel",]);
+ r.unwrap_err();
+ let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--kernel",]);
+ r.unwrap_err();
+ }
}
diff --git a/cli/main.rs b/cli/main.rs
index df5dd0b26..98a2e5d48 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -134,6 +134,9 @@ async fn run_subcommand(flags: Flags) -> Result<i32, AnyError> {
DenoSubcommand::Install(install_flags) => spawn_subcommand(async {
tools::installer::install_command(flags, install_flags).await
}),
+ DenoSubcommand::Jupyter(jupyter_flags) => spawn_subcommand(async {
+ tools::jupyter::kernel(flags, jupyter_flags).await
+ }),
DenoSubcommand::Uninstall(uninstall_flags) => spawn_subcommand(async {
tools::installer::uninstall(uninstall_flags.name, uninstall_flags.root)
}),
diff --git a/cli/module_loader.rs b/cli/module_loader.rs
index 67811304b..4a1e0b671 100644
--- a/cli/module_loader.rs
+++ b/cli/module_loader.rs
@@ -330,7 +330,10 @@ impl CliModuleLoaderFactory {
lib_window: options.ts_type_lib_window(),
lib_worker: options.ts_type_lib_worker(),
is_inspecting: options.is_inspecting(),
- is_repl: matches!(options.sub_command(), DenoSubcommand::Repl(_)),
+ is_repl: matches!(
+ options.sub_command(),
+ DenoSubcommand::Repl(_) | DenoSubcommand::Jupyter(_)
+ ),
prepared_module_loader: PreparedModuleLoader {
emitter,
graph_container: graph_container.clone(),
diff --git a/cli/tests/testdata/jupyter/integration_test.ipynb b/cli/tests/testdata/jupyter/integration_test.ipynb
new file mode 100644
index 000000000..ec6b27973
--- /dev/null
+++ b/cli/tests/testdata/jupyter/integration_test.ipynb
@@ -0,0 +1,620 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "182aef1d",
+ "metadata": {},
+ "source": [
+ "# Integration Tests for Deno Jupyter\n",
+ "This notebook contains a number of tests to ensure that Jupyter is working as expected. You should be able to select \"Kernel -> Restart Kernel and Run All\" in Jupyter's notebook UI to run the tests."
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "d7705d88",
+ "metadata": {},
+ "source": [
+ "## Passing Tests"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "669f972e",
+ "metadata": {
+ "heading_collapsed": true
+ },
+ "source": [
+ "### Simple Tests"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "e7e8a512",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### This test should print \"hi\".\n",
+ "If this doesn't work, everything else will probably fail :)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "a5d38758",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {},
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "hi\n"
+ ]
+ }
+ ],
+ "source": [
+ "console.log(\"hi\")"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "bc5ce8e3",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### Top-level await"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "f7fa885a",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {},
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "x is \u001b[33m42\u001b[39m\n"
+ ]
+ }
+ ],
+ "source": [
+ "let x = await Promise.resolve(42);\n",
+ "console.log(\"x is\", x);"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "c21455ae",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### TypeScript transpiling\n",
+ "Credit to [typescriptlang.org](https://www.typescriptlang.org/docs/handbook/interfaces.html) for this code"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "08a17340",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{ color: \u001b[32m\"red\"\u001b[39m, area: \u001b[33m10000\u001b[39m }"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "interface SquareConfig {\n",
+ " color?: string;\n",
+ " width?: number;\n",
+ "}\n",
+ " \n",
+ "function createSquare(config: SquareConfig): { color: string; area: number } {\n",
+ " return {\n",
+ " color: config.color || \"red\",\n",
+ " area: config.width ? config.width * config.width : 20,\n",
+ " };\n",
+ "}\n",
+ " \n",
+ "createSquare({ colour: \"red\", width: 100 });"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "eaa0ebc0",
+ "metadata": {
+ "heading_collapsed": true
+ },
+ "source": [
+ "### Return Values"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "52876276",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### undefined should not return a value"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "bbf2c09b",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {},
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "undefined"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "e175c803",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### null should return \"null\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "d9801d80",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[1mnull\u001b[22m"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "null"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "a2a716dc",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### boolean should return the boolean"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "cfaac330",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[33mtrue\u001b[39m"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "true"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "8d9f1aba",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### number should return the number"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "ec3be2da",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[33m42\u001b[39m"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "42"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "60965915",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### string should return the string"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "997cf2d7",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[32m\"this is a test of the emergency broadcast system\"\u001b[39m"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\"this is a test of the emergency broadcast system\""
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "fe38dc27",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### bigint should return the bigint in literal format"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "44b63807",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[33m31337n\u001b[39m"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "31337n"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "843ccb6c",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### symbol should return a string describing the symbol"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "e10c0d31",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[32mSymbol(foo)\u001b[39m"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Symbol(\"foo\")"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "171b817f",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### object should describe the object inspection"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "81c99233",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{ foo: \u001b[32m\"bar\"\u001b[39m }"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "{foo: \"bar\"}"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "6caeb583",
+ "metadata": {
+ "hidden": true
+ },
+ "source": [
+ "#### resolve returned promise"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "43c1581b",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Promise { \u001b[32m\"it worked!\"\u001b[39m }"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Promise.resolve(\"it worked!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "9a34b725",
+ "metadata": {
+ "hidden": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Promise {\n",
+ " \u001b[36m<rejected>\u001b[39m Error: it failed!\n",
+ " at <anonymous>:2:16\n",
+ "}"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Promise.reject(new Error(\"it failed!\"));"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b5c7b819",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "ename": "Error: this is a test\n at foo (<anonymous>:3:9)\n at <anonymous>:4:3",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": []
+ }
+ ],
+ "source": [
+ "(function foo() {\n",
+ " throw new Error(\"this is a test\")\n",
+ "})()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "72d01fdd",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Promise {\n",
+ " \u001b[36m<rejected>\u001b[39m TypeError: Expected string at position 0\n",
+ " at Object.readFile (ext:deno_fs/30_fs.js:716:29)\n",
+ " at <anonymous>:2:6\n",
+ "}"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Deno.readFile(1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "28cf59d0-6908-4edc-bb10-c325beeee362",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(\"Hello from Deno!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8d5485c3-0da3-43fe-8ef5-a61e672f5e81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(\"%c Hello Deno \", \"background-color: #15803d; color: white;\");"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1401d9d5-6994-4c7b-b55a-db3c16a1e2dc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\"Cool 🫡\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7afdaa0a-a2a0-4f52-8c7d-b6c5f237aa0d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.table([1, 2, 3])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8e93df23-06eb-414b-98d4-51fbebb53d1f",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Deno",
+ "language": "typescript",
+ "name": "deno"
+ },
+ "language_info": {
+ "file_extension": ".ts",
+ "mimetype": "text/x.typescript",
+ "name": "typescript",
+ "nb_converter": "script",
+ "pygments_lexer": "typescript",
+ "version": "5.2.2"
+ },
+ "toc": {
+ "base_numbering": 1,
+ "nav_menu": {},
+ "number_sections": true,
+ "sideBar": true,
+ "skip_h1_title": false,
+ "title_cell": "Table of Contents",
+ "title_sidebar": "Contents",
+ "toc_cell": false,
+ "toc_position": {},
+ "toc_section_display": true,
+ "toc_window_display": true
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/cli/tools/jupyter/install.rs b/cli/tools/jupyter/install.rs
new file mode 100644
index 000000000..d1777d92d
--- /dev/null
+++ b/cli/tools/jupyter/install.rs
@@ -0,0 +1,95 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use std::env::current_exe;
+use std::io::Write;
+use std::path::Path;
+use tempfile::TempDir;
+
+const DENO_ICON_32: &[u8] = include_bytes!("./resources/deno-logo-32x32.png");
+const DENO_ICON_64: &[u8] = include_bytes!("./resources/deno-logo-64x64.png");
+
+pub fn status() -> Result<(), AnyError> {
+ let output = std::process::Command::new("jupyter")
+ .args(["kernelspec", "list", "--json"])
+ .output()
+ .context("Failed to get list of installed kernelspecs")?;
+ let json_output: serde_json::Value =
+ serde_json::from_slice(&output.stdout)
+ .context("Failed to parse JSON from kernelspec list")?;
+
+ if let Some(specs) = json_output.get("kernelspecs") {
+ if let Some(specs_obj) = specs.as_object() {
+ if specs_obj.contains_key("deno") {
+ println!("✅ Deno kernel already installed");
+ return Ok(());
+ }
+ }
+ }
+
+ println!("ℹ️ Deno kernel is not yet installed, run `deno jupyter --unstable --install` to set it up");
+ Ok(())
+}
+
+fn install_icon(
+ dir_path: &Path,
+ filename: &str,
+ icon_data: &[u8],
+) -> Result<(), AnyError> {
+ let path = dir_path.join(filename);
+ let mut file = std::fs::File::create(path)?;
+ file.write_all(icon_data)?;
+ Ok(())
+}
+
+pub fn install() -> Result<(), AnyError> {
+ let temp_dir = TempDir::new().unwrap();
+ let kernel_json_path = temp_dir.path().join("kernel.json");
+
+ // TODO(bartlomieju): add remaining fields as per
+ // https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs
+ // FIXME(bartlomieju): replace `current_exe` before landing?
+ let json_data = json!({
+ "argv": [current_exe().unwrap().to_string_lossy(), "--unstable", "jupyter", "--kernel", "--conn", "{connection_file}"],
+ "display_name": "Deno",
+ "language": "typescript",
+ });
+
+ let f = std::fs::File::create(kernel_json_path)?;
+ serde_json::to_writer_pretty(f, &json_data)?;
+ install_icon(temp_dir.path(), "icon-32x32.png", DENO_ICON_32)?;
+ install_icon(temp_dir.path(), "icon-64x64.png", DENO_ICON_64)?;
+
+ let child_result = std::process::Command::new("jupyter")
+ .args([
+ "kernelspec",
+ "install",
+ "--user",
+ "--name",
+ "deno",
+ &temp_dir.path().to_string_lossy(),
+ ])
+ .spawn();
+
+ if let Ok(mut child) = child_result {
+ let wait_result = child.wait();
+ match wait_result {
+ Ok(status) => {
+ if !status.success() {
+ bail!("Failed to install kernelspec, try again.");
+ }
+ }
+ Err(err) => {
+ bail!("Failed to install kernelspec: {}", err);
+ }
+ }
+ }
+
+ let _ = std::fs::remove_dir(temp_dir);
+ println!("✅ Deno kernelspec installed successfully.");
+ Ok(())
+}
diff --git a/cli/tools/jupyter/jupyter_msg.rs b/cli/tools/jupyter/jupyter_msg.rs
new file mode 100644
index 000000000..c28dd3b48
--- /dev/null
+++ b/cli/tools/jupyter/jupyter_msg.rs
@@ -0,0 +1,268 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// This file is forked/ported from <https://github.com/evcxr/evcxr>
+// Copyright 2020 The Evcxr Authors. MIT license.
+
+use bytes::Bytes;
+use chrono::Utc;
+use data_encoding::HEXLOWER;
+use deno_core::anyhow::anyhow;
+use deno_core::anyhow::bail;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use ring::hmac;
+use std::fmt;
+use uuid::Uuid;
+
+pub(crate) struct Connection<S> {
+ pub(crate) socket: S,
+ /// Will be None if our key was empty (digest authentication disabled).
+ pub(crate) mac: Option<hmac::Key>,
+}
+
+impl<S: zeromq::Socket> Connection<S> {
+ pub(crate) fn new(socket: S, key: &str) -> Self {
+ let mac = if key.is_empty() {
+ None
+ } else {
+ Some(hmac::Key::new(hmac::HMAC_SHA256, key.as_bytes()))
+ };
+ Connection { socket, mac }
+ }
+}
+
+struct RawMessage {
+ zmq_identities: Vec<Bytes>,
+ jparts: Vec<Bytes>,
+}
+
+impl RawMessage {
+ pub(crate) async fn read<S: zeromq::SocketRecv>(
+ connection: &mut Connection<S>,
+ ) -> Result<RawMessage, AnyError> {
+ Self::from_multipart(connection.socket.recv().await?, connection)
+ }
+
+ pub(crate) fn from_multipart<S>(
+ multipart: zeromq::ZmqMessage,
+ connection: &Connection<S>,
+ ) -> Result<RawMessage, AnyError> {
+ let delimiter_index = multipart
+ .iter()
+ .position(|part| &part[..] == DELIMITER)
+ .ok_or_else(|| anyhow!("Missing delimiter"))?;
+ let mut parts = multipart.into_vec();
+ let jparts: Vec<_> = parts.drain(delimiter_index + 2..).collect();
+ let expected_hmac = parts.pop().unwrap();
+ // Remove delimiter, so that what's left is just the identities.
+ parts.pop();
+ let zmq_identities = parts;
+
+ let raw_message = RawMessage {
+ zmq_identities,
+ jparts,
+ };
+
+ if let Some(key) = &connection.mac {
+ let sig = HEXLOWER.decode(&expected_hmac)?;
+ let mut msg = Vec::new();
+ for part in &raw_message.jparts {
+ msg.extend(part);
+ }
+
+ if let Err(err) = hmac::verify(key, msg.as_ref(), sig.as_ref()) {
+ bail!("{}", err);
+ }
+ }
+
+ Ok(raw_message)
+ }
+
+ async fn send<S: zeromq::SocketSend>(
+ self,
+ connection: &mut Connection<S>,
+ ) -> Result<(), AnyError> {
+ let hmac = if let Some(key) = &connection.mac {
+ let ctx = self.digest(key);
+ let tag = ctx.sign();
+ HEXLOWER.encode(tag.as_ref())
+ } else {
+ String::new()
+ };
+ let mut parts: Vec<bytes::Bytes> = Vec::new();
+ for part in &self.zmq_identities {
+ parts.push(part.to_vec().into());
+ }
+ parts.push(DELIMITER.into());
+ parts.push(hmac.as_bytes().to_vec().into());
+ for part in &self.jparts {
+ parts.push(part.to_vec().into());
+ }
+ // ZmqMessage::try_from only fails if parts is empty, which it never
+ // will be here.
+ let message = zeromq::ZmqMessage::try_from(parts).unwrap();
+ connection.socket.send(message).await?;
+ Ok(())
+ }
+
+ fn digest(&self, mac: &hmac::Key) -> hmac::Context {
+ let mut hmac_ctx = hmac::Context::with_key(mac);
+ for part in &self.jparts {
+ hmac_ctx.update(part);
+ }
+ hmac_ctx
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct JupyterMessage {
+ zmq_identities: Vec<Bytes>,
+ header: serde_json::Value,
+ parent_header: serde_json::Value,
+ metadata: serde_json::Value,
+ content: serde_json::Value,
+}
+
+const DELIMITER: &[u8] = b"<IDS|MSG>";
+
+impl JupyterMessage {
+ pub(crate) async fn read<S: zeromq::SocketRecv>(
+ connection: &mut Connection<S>,
+ ) -> Result<JupyterMessage, AnyError> {
+ Self::from_raw_message(RawMessage::read(connection).await?)
+ }
+
+ fn from_raw_message(
+ raw_message: RawMessage,
+ ) -> Result<JupyterMessage, AnyError> {
+ if raw_message.jparts.len() < 4 {
+ bail!("Insufficient message parts {}", raw_message.jparts.len());
+ }
+
+ Ok(JupyterMessage {
+ zmq_identities: raw_message.zmq_identities,
+ header: serde_json::from_slice(&raw_message.jparts[0])?,
+ parent_header: serde_json::from_slice(&raw_message.jparts[1])?,
+ metadata: serde_json::from_slice(&raw_message.jparts[2])?,
+ content: serde_json::from_slice(&raw_message.jparts[3])?,
+ })
+ }
+
+ pub(crate) fn message_type(&self) -> &str {
+ self.header["msg_type"].as_str().unwrap_or("")
+ }
+
+ pub(crate) fn code(&self) -> &str {
+ self.content["code"].as_str().unwrap_or("")
+ }
+
+ pub(crate) fn cursor_pos(&self) -> usize {
+ self.content["cursor_pos"].as_u64().unwrap_or(0) as usize
+ }
+
+ pub(crate) fn comm_id(&self) -> &str {
+ self.content["comm_id"].as_str().unwrap_or("")
+ }
+
+ // Creates a new child message of this message. ZMQ identities are not transferred.
+ pub(crate) fn new_message(&self, msg_type: &str) -> JupyterMessage {
+ let mut header = self.header.clone();
+ header["msg_type"] = serde_json::Value::String(msg_type.to_owned());
+ header["username"] = serde_json::Value::String("kernel".to_owned());
+ header["msg_id"] = serde_json::Value::String(Uuid::new_v4().to_string());
+ header["date"] = serde_json::Value::String(Utc::now().to_rfc3339());
+
+ JupyterMessage {
+ zmq_identities: Vec::new(),
+ header,
+ parent_header: self.header.clone(),
+ metadata: json!({}),
+ content: json!({}),
+ }
+ }
+
+ // Creates a reply to this message. This is a child with the message type determined
+ // automatically by replacing "request" with "reply". ZMQ identities are transferred.
+ pub(crate) fn new_reply(&self) -> JupyterMessage {
+ let mut reply =
+ self.new_message(&self.message_type().replace("_request", "_reply"));
+ reply.zmq_identities = self.zmq_identities.clone();
+ reply
+ }
+
+ #[must_use = "Need to send this message for it to have any effect"]
+ pub(crate) fn comm_close_message(&self) -> JupyterMessage {
+ self.new_message("comm_close").with_content(json!({
+ "comm_id": self.comm_id()
+ }))
+ }
+
+ pub(crate) fn with_content(
+ mut self,
+ content: serde_json::Value,
+ ) -> JupyterMessage {
+ self.content = content;
+ self
+ }
+
+ pub(crate) async fn send<S: zeromq::SocketSend>(
+ &self,
+ connection: &mut Connection<S>,
+ ) -> Result<(), AnyError> {
+ // If performance is a concern, we can probably avoid the clone and to_vec calls with a bit
+ // of refactoring.
+ let raw_message = RawMessage {
+ zmq_identities: self.zmq_identities.clone(),
+ jparts: vec![
+ serde_json::to_string(&self.header)
+ .unwrap()
+ .as_bytes()
+ .to_vec()
+ .into(),
+ serde_json::to_string(&self.parent_header)
+ .unwrap()
+ .as_bytes()
+ .to_vec()
+ .into(),
+ serde_json::to_string(&self.metadata)
+ .unwrap()
+ .as_bytes()
+ .to_vec()
+ .into(),
+ serde_json::to_string(&self.content)
+ .unwrap()
+ .as_bytes()
+ .to_vec()
+ .into(),
+ ],
+ };
+ raw_message.send(connection).await
+ }
+}
+
+impl fmt::Debug for JupyterMessage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(
+ f,
+ "\nHeader: {}",
+ serde_json::to_string_pretty(&self.header).unwrap()
+ )?;
+ writeln!(
+ f,
+ "Parent header: {}",
+ serde_json::to_string_pretty(&self.parent_header).unwrap()
+ )?;
+ writeln!(
+ f,
+ "Metadata: {}",
+ serde_json::to_string_pretty(&self.metadata).unwrap()
+ )?;
+ writeln!(
+ f,
+ "Content: {}\n",
+ serde_json::to_string_pretty(&self.content).unwrap()
+ )?;
+ Ok(())
+ }
+}
diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs
new file mode 100644
index 000000000..b704d58cd
--- /dev/null
+++ b/cli/tools/jupyter/mod.rs
@@ -0,0 +1,139 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use crate::args::Flags;
+use crate::args::JupyterFlags;
+use crate::tools::repl;
+use crate::util::logger;
+use crate::CliFactory;
+use deno_core::anyhow::Context;
+use deno_core::error::AnyError;
+use deno_core::futures::channel::mpsc;
+use deno_core::op;
+use deno_core::resolve_url_or_path;
+use deno_core::serde::Deserialize;
+use deno_core::serde_json;
+use deno_core::Op;
+use deno_core::OpState;
+use deno_runtime::permissions::Permissions;
+use deno_runtime::permissions::PermissionsContainer;
+
+mod install;
+mod jupyter_msg;
+mod server;
+
+pub async fn kernel(
+ flags: Flags,
+ jupyter_flags: JupyterFlags,
+) -> Result<(), AnyError> {
+ if !flags.unstable {
+ eprintln!(
+ "Unstable subcommand 'deno jupyter'. The --unstable flag must be provided."
+ );
+ std::process::exit(70);
+ }
+
+ if !jupyter_flags.install && !jupyter_flags.kernel {
+ install::status()?;
+ return Ok(());
+ }
+
+ if jupyter_flags.install {
+ install::install()?;
+ return Ok(());
+ }
+
+ let connection_filepath = jupyter_flags.conn_file.unwrap();
+
+ // This env var might be set by notebook
+ if std::env::var("DEBUG").is_ok() {
+ logger::init(Some(log::Level::Debug));
+ }
+
+ let factory = CliFactory::from_flags(flags).await?;
+ let cli_options = factory.cli_options();
+ let main_module =
+ resolve_url_or_path("./$deno$jupyter.ts", cli_options.initial_cwd())
+ .unwrap();
+ // TODO(bartlomieju): should we run with all permissions?
+ let permissions = PermissionsContainer::new(Permissions::allow_all());
+ let npm_resolver = factory.npm_resolver().await?.clone();
+ let resolver = factory.resolver().await?.clone();
+ let worker_factory = factory.create_cli_main_worker_factory().await?;
+ let (stdio_tx, stdio_rx) = mpsc::unbounded();
+
+ let conn_file =
+ std::fs::read_to_string(&connection_filepath).with_context(|| {
+ format!("Couldn't read connection file: {:?}", connection_filepath)
+ })?;
+ let spec: ConnectionSpec =
+ serde_json::from_str(&conn_file).with_context(|| {
+ format!(
+ "Connection file is not a valid JSON: {:?}",
+ connection_filepath
+ )
+ })?;
+
+ let mut worker = worker_factory
+ .create_custom_worker(
+ main_module.clone(),
+ permissions,
+ vec![deno_jupyter::init_ops(stdio_tx)],
+ Default::default(),
+ )
+ .await?;
+ worker.setup_repl().await?;
+ let worker = worker.into_main_worker();
+ let repl_session =
+ repl::ReplSession::initialize(cli_options, npm_resolver, resolver, worker)
+ .await?;
+
+ server::JupyterServer::start(spec, stdio_rx, repl_session).await?;
+
+ Ok(())
+}
+
+deno_core::extension!(deno_jupyter,
+ options = {
+ sender: mpsc::UnboundedSender<server::StdioMsg>,
+ },
+ middleware = |op| match op.name {
+ "op_print" => op_print::DECL,
+ _ => op,
+ },
+ state = |state, options| {
+ state.put(options.sender);
+ },
+);
+
+#[op]
+pub fn op_print(
+ state: &mut OpState,
+ msg: String,
+ is_err: bool,
+) -> Result<(), AnyError> {
+ let sender = state.borrow_mut::<mpsc::UnboundedSender<server::StdioMsg>>();
+
+ if is_err {
+ if let Err(err) = sender.unbounded_send(server::StdioMsg::Stderr(msg)) {
+ eprintln!("Failed to send stderr message: {}", err);
+ }
+ return Ok(());
+ }
+
+ if let Err(err) = sender.unbounded_send(server::StdioMsg::Stdout(msg)) {
+ eprintln!("Failed to send stdout message: {}", err);
+ }
+ Ok(())
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ConnectionSpec {
+ ip: String,
+ transport: String,
+ control_port: u32,
+ shell_port: u32,
+ stdin_port: u32,
+ hb_port: u32,
+ iopub_port: u32,
+ key: String,
+}
diff --git a/cli/tools/jupyter/resources/deno-logo-32x32.png b/cli/tools/jupyter/resources/deno-logo-32x32.png
new file mode 100644
index 000000000..97871a02e
--- /dev/null
+++ b/cli/tools/jupyter/resources/deno-logo-32x32.png
Binary files differ
diff --git a/cli/tools/jupyter/resources/deno-logo-64x64.png b/cli/tools/jupyter/resources/deno-logo-64x64.png
new file mode 100644
index 000000000..1b9444ef6
--- /dev/null
+++ b/cli/tools/jupyter/resources/deno-logo-64x64.png
Binary files differ
diff --git a/cli/tools/jupyter/server.rs b/cli/tools/jupyter/server.rs
new file mode 100644
index 000000000..c15dab6c2
--- /dev/null
+++ b/cli/tools/jupyter/server.rs
@@ -0,0 +1,724 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// This file is forked/ported from <https://github.com/evcxr/evcxr>
+// Copyright 2020 The Evcxr Authors. MIT license.
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::sync::Arc;
+
+use crate::tools::repl;
+use crate::tools::repl::cdp;
+use deno_core::error::AnyError;
+use deno_core::futures;
+use deno_core::futures::channel::mpsc;
+use deno_core::futures::StreamExt;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use tokio::sync::Mutex;
+use zeromq::SocketRecv;
+use zeromq::SocketSend;
+
+use super::jupyter_msg::Connection;
+use super::jupyter_msg::JupyterMessage;
+use super::ConnectionSpec;
+
+pub enum StdioMsg {
+ Stdout(String),
+ Stderr(String),
+}
+
+pub struct JupyterServer {
+ execution_count: usize,
+ last_execution_request: Rc<RefCell<Option<JupyterMessage>>>,
+ // This is Arc<Mutex<>>, so we don't hold RefCell borrows across await
+ // points.
+ iopub_socket: Arc<Mutex<Connection<zeromq::PubSocket>>>,
+ repl_session: repl::ReplSession,
+}
+
+impl JupyterServer {
+ pub async fn start(
+ spec: ConnectionSpec,
+ mut stdio_rx: mpsc::UnboundedReceiver<StdioMsg>,
+ repl_session: repl::ReplSession,
+ ) -> Result<(), AnyError> {
+ let mut heartbeat =
+ bind_socket::<zeromq::RepSocket>(&spec, spec.hb_port).await?;
+ let shell_socket =
+ bind_socket::<zeromq::RouterSocket>(&spec, spec.shell_port).await?;
+ let control_socket =
+ bind_socket::<zeromq::RouterSocket>(&spec, spec.control_port).await?;
+ let _stdin_socket =
+ bind_socket::<zeromq::RouterSocket>(&spec, spec.stdin_port).await?;
+ let iopub_socket =
+ bind_socket::<zeromq::PubSocket>(&spec, spec.iopub_port).await?;
+ let iopub_socket = Arc::new(Mutex::new(iopub_socket));
+ let last_execution_request = Rc::new(RefCell::new(None));
+
+ let cancel_handle = CancelHandle::new_rc();
+ let cancel_handle2 = CancelHandle::new_rc();
+
+ let mut server = Self {
+ execution_count: 0,
+ iopub_socket: iopub_socket.clone(),
+ last_execution_request: last_execution_request.clone(),
+ repl_session,
+ };
+
+ let handle1 = deno_core::unsync::spawn(async move {
+ if let Err(err) = Self::handle_heartbeat(&mut heartbeat).await {
+ eprintln!("Heartbeat error: {}", err);
+ }
+ });
+
+ let handle2 = deno_core::unsync::spawn(async move {
+ if let Err(err) =
+ Self::handle_control(control_socket, cancel_handle2).await
+ {
+ eprintln!("Control error: {}", err);
+ }
+ });
+
+ let handle3 = deno_core::unsync::spawn(async move {
+ if let Err(err) = server.handle_shell(shell_socket).await {
+ eprintln!("Shell error: {}", err);
+ }
+ });
+
+ let handle4 = deno_core::unsync::spawn(async move {
+ while let Some(stdio_msg) = stdio_rx.next().await {
+ Self::handle_stdio_msg(
+ iopub_socket.clone(),
+ last_execution_request.clone(),
+ stdio_msg,
+ )
+ .await;
+ }
+ });
+
+ let join_fut =
+ futures::future::try_join_all(vec![handle1, handle2, handle3, handle4]);
+
+ if let Ok(result) = join_fut.or_cancel(cancel_handle).await {
+ result?;
+ }
+
+ Ok(())
+ }
+
+ async fn handle_stdio_msg<S: zeromq::SocketSend>(
+ iopub_socket: Arc<Mutex<Connection<S>>>,
+ last_execution_request: Rc<RefCell<Option<JupyterMessage>>>,
+ stdio_msg: StdioMsg,
+ ) {
+ let maybe_exec_result = last_execution_request.borrow().clone();
+ if let Some(exec_request) = maybe_exec_result {
+ let (name, text) = match stdio_msg {
+ StdioMsg::Stdout(text) => ("stdout", text),
+ StdioMsg::Stderr(text) => ("stderr", text),
+ };
+
+ let result = exec_request
+ .new_message("stream")
+ .with_content(json!({
+ "name": name,
+ "text": text
+ }))
+ .send(&mut *iopub_socket.lock().await)
+ .await;
+
+ if let Err(err) = result {
+ eprintln!("Output {} error: {}", name, err);
+ }
+ }
+ }
+
+ async fn handle_heartbeat(
+ connection: &mut Connection<zeromq::RepSocket>,
+ ) -> Result<(), AnyError> {
+ loop {
+ connection.socket.recv().await?;
+ connection
+ .socket
+ .send(zeromq::ZmqMessage::from(b"ping".to_vec()))
+ .await?;
+ }
+ }
+
+ async fn handle_control(
+ mut connection: Connection<zeromq::RouterSocket>,
+ cancel_handle: Rc<CancelHandle>,
+ ) -> Result<(), AnyError> {
+ loop {
+ let msg = JupyterMessage::read(&mut connection).await?;
+ match msg.message_type() {
+ "kernel_info_request" => {
+ msg
+ .new_reply()
+ .with_content(kernel_info())
+ .send(&mut connection)
+ .await?;
+ }
+ "shutdown_request" => {
+ cancel_handle.cancel();
+ }
+ "interrupt_request" => {
+ eprintln!("Interrupt request currently not supported");
+ }
+ _ => {
+ eprintln!(
+ "Unrecognized control message type: {}",
+ msg.message_type()
+ );
+ }
+ }
+ }
+ }
+
+ async fn handle_shell(
+ &mut self,
+ mut connection: Connection<zeromq::RouterSocket>,
+ ) -> Result<(), AnyError> {
+ loop {
+ let msg = JupyterMessage::read(&mut connection).await?;
+ self.handle_shell_message(msg, &mut connection).await?;
+ }
+ }
+
+ async fn handle_shell_message(
+ &mut self,
+ msg: JupyterMessage,
+ connection: &mut Connection<zeromq::RouterSocket>,
+ ) -> Result<(), AnyError> {
+ msg
+ .new_message("status")
+ .with_content(json!({"execution_state": "busy"}))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+
+ match msg.message_type() {
+ "kernel_info_request" => {
+ msg
+ .new_reply()
+ .with_content(kernel_info())
+ .send(connection)
+ .await?;
+ }
+ "is_complete_request" => {
+ msg
+ .new_reply()
+ .with_content(json!({"status": "complete"}))
+ .send(connection)
+ .await?;
+ }
+ "execute_request" => {
+ self
+ .handle_execution_request(msg.clone(), connection)
+ .await?;
+ }
+ "comm_open" => {
+ msg
+ .comm_close_message()
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+ }
+ "complete_request" => {
+ let user_code = msg.code();
+ let cursor_pos = msg.cursor_pos();
+
+ let lsp_completions = self
+ .repl_session
+ .language_server
+ .completions(user_code, cursor_pos)
+ .await;
+
+ if !lsp_completions.is_empty() {
+ let matches: Vec<String> = lsp_completions
+ .iter()
+ .map(|item| item.new_text.clone())
+ .collect();
+
+ let cursor_start = lsp_completions
+ .first()
+ .map(|item| item.range.start)
+ .unwrap_or(cursor_pos);
+
+ let cursor_end = lsp_completions
+ .last()
+ .map(|item| item.range.end)
+ .unwrap_or(cursor_pos);
+
+ msg
+ .new_reply()
+ .with_content(json!({
+ "status": "ok",
+ "matches": matches,
+ "cursor_start": cursor_start,
+ "cursor_end": cursor_end,
+ "metadata": {},
+ }))
+ .send(connection)
+ .await?;
+ } else {
+ let expr = get_expr_from_line_at_pos(user_code, cursor_pos);
+ // check if the expression is in the form `obj.prop`
+ let (completions, cursor_start) = if let Some(index) = expr.rfind('.')
+ {
+ let sub_expr = &expr[..index];
+ let prop_name = &expr[index + 1..];
+ let candidates =
+ get_expression_property_names(&mut self.repl_session, sub_expr)
+ .await
+ .into_iter()
+ .filter(|n| {
+ !n.starts_with("Symbol(")
+ && n.starts_with(prop_name)
+ && n != &*repl::REPL_INTERNALS_NAME
+ })
+ .collect();
+
+ (candidates, cursor_pos - prop_name.len())
+ } else {
+ // combine results of declarations and globalThis properties
+ let mut candidates = get_expression_property_names(
+ &mut self.repl_session,
+ "globalThis",
+ )
+ .await
+ .into_iter()
+ .chain(get_global_lexical_scope_names(&mut self.repl_session).await)
+ .filter(|n| n.starts_with(expr) && n != &*repl::REPL_INTERNALS_NAME)
+ .collect::<Vec<_>>();
+
+ // sort and remove duplicates
+ candidates.sort();
+ candidates.dedup(); // make sure to sort first
+
+ (candidates, cursor_pos - expr.len())
+ };
+ msg
+ .new_reply()
+ .with_content(json!({
+ "status": "ok",
+ "matches": completions,
+ "cursor_start": cursor_start,
+ "cursor_end": cursor_pos,
+ "metadata": {},
+ }))
+ .send(connection)
+ .await?;
+ }
+ }
+ "comm_msg" | "comm_info_request" | "history_request" => {
+ // We don't handle these messages
+ }
+ _ => {
+ eprintln!("Unrecognized shell message type: {}", msg.message_type());
+ }
+ }
+
+ msg
+ .new_message("status")
+ .with_content(json!({"execution_state": "idle"}))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+ Ok(())
+ }
+
+ async fn handle_execution_request(
+ &mut self,
+ msg: JupyterMessage,
+ connection: &mut Connection<zeromq::RouterSocket>,
+ ) -> Result<(), AnyError> {
+ self.execution_count += 1;
+ *self.last_execution_request.borrow_mut() = Some(msg.clone());
+
+ msg
+ .new_message("execute_input")
+ .with_content(json!({
+ "execution_count": self.execution_count,
+ "code": msg.code()
+ }))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+
+ let result = self
+ .repl_session
+ .evaluate_line_with_object_wrapping(msg.code())
+ .await;
+
+ let evaluate_response = match result {
+ Ok(eval_response) => eval_response,
+ Err(err) => {
+ msg
+ .new_message("error")
+ .with_content(json!({
+ "ename": err.to_string(),
+ "evalue": "",
+ "traceback": [],
+ }))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+ msg
+ .new_reply()
+ .with_content(json!({
+ "status": "error",
+ "execution_count": self.execution_count,
+ }))
+ .send(connection)
+ .await?;
+ return Ok(());
+ }
+ };
+
+ let repl::cdp::EvaluateResponse {
+ result,
+ exception_details,
+ } = evaluate_response.value;
+
+ if exception_details.is_none() {
+ let output =
+ get_jupyter_display_or_eval_value(&mut self.repl_session, &result)
+ .await?;
+ msg
+ .new_message("execute_result")
+ .with_content(json!({
+ "execution_count": self.execution_count,
+ "data": output,
+ "metadata": {},
+ }))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+ msg
+ .new_reply()
+ .with_content(json!({
+ "status": "ok",
+ "execution_count": self.execution_count,
+ }))
+ .send(connection)
+ .await?;
+ // Let's sleep here for a few ms, so we give a chance to the task that is
+ // handling stdout and stderr streams to receive and flush the content.
+ // Otherwise, executing multiple cells one-by-one might lead to output
+ // from various cells be grouped together in another cell result.
+ tokio::time::sleep(std::time::Duration::from_millis(5)).await;
+ } else {
+ let exception_details = exception_details.unwrap();
+ let name = if let Some(exception) = exception_details.exception {
+ if let Some(description) = exception.description {
+ description
+ } else if let Some(value) = exception.value {
+ value.to_string()
+ } else {
+ "undefined".to_string()
+ }
+ } else {
+ "Unknown exception".to_string()
+ };
+
+ // TODO(bartlomieju): fill all the fields
+ msg
+ .new_message("error")
+ .with_content(json!({
+ "ename": name,
+ "evalue": "",
+ "traceback": [],
+ }))
+ .send(&mut *self.iopub_socket.lock().await)
+ .await?;
+ msg
+ .new_reply()
+ .with_content(json!({
+ "status": "error",
+ "execution_count": self.execution_count,
+ }))
+ .send(connection)
+ .await?;
+ }
+
+ Ok(())
+ }
+}
+
+async fn bind_socket<S: zeromq::Socket>(
+ config: &ConnectionSpec,
+ port: u32,
+) -> Result<Connection<S>, AnyError> {
+ let endpoint = format!("{}://{}:{}", config.transport, config.ip, port);
+ let mut socket = S::new();
+ socket.bind(&endpoint).await?;
+ Ok(Connection::new(socket, &config.key))
+}
+
+fn kernel_info() -> serde_json::Value {
+ json!({
+ "status": "ok",
+ "protocol_version": "5.3",
+ "implementation_version": crate::version::deno(),
+ "implementation": "Deno kernel",
+ "language_info": {
+ "name": "typescript",
+ "version": crate::version::TYPESCRIPT,
+ "mimetype": "text/x.typescript",
+ "file_extension": ".ts",
+ "pygments_lexer": "typescript",
+ "nb_converter": "script"
+ },
+ "help_links": [{
+ "text": "Visit Deno manual",
+ "url": "https://deno.land/manual"
+ }],
+ "banner": "Welcome to Deno kernel",
+ })
+}
+
+async fn get_jupyter_display(
+ session: &mut repl::ReplSession,
+ evaluate_result: &cdp::RemoteObject,
+) -> Result<Option<HashMap<String, serde_json::Value>>, AnyError> {
+ let mut data = HashMap::default();
+ let response = session
+ .call_function_on_args(
+ r#"function (object) {{
+ return object[Symbol.for("Jupyter.display")]();
+ }}"#
+ .to_string(),
+ &[evaluate_result.clone()],
+ )
+ .await?;
+
+ if response.exception_details.is_some() {
+ return Ok(None);
+ }
+
+ let object_id = response.result.object_id.unwrap();
+
+ let get_properties_response_result = session
+ .post_message_with_event_loop(
+ "Runtime.getProperties",
+ Some(cdp::GetPropertiesArgs {
+ object_id,
+ own_properties: Some(true),
+ accessor_properties_only: None,
+ generate_preview: None,
+ non_indexed_properties_only: Some(true),
+ }),
+ )
+ .await;
+
+ let Ok(get_properties_response) = get_properties_response_result else {
+ return Ok(None);
+ };
+
+ let get_properties_response: cdp::GetPropertiesResponse =
+ serde_json::from_value(get_properties_response).unwrap();
+
+ for prop in get_properties_response.result.into_iter() {
+ if let Some(value) = &prop.value {
+ data.insert(
+ prop.name.to_string(),
+ value
+ .value
+ .clone()
+ .unwrap_or_else(|| serde_json::Value::Null),
+ );
+ }
+ }
+
+ if !data.is_empty() {
+ return Ok(Some(data));
+ }
+
+ Ok(None)
+}
+
+async fn get_jupyter_display_or_eval_value(
+ session: &mut repl::ReplSession,
+ evaluate_result: &cdp::RemoteObject,
+) -> Result<HashMap<String, serde_json::Value>, AnyError> {
+ // Printing "undefined" generates a lot of noise, so let's skip
+ // these.
+ if evaluate_result.kind == "undefined" {
+ return Ok(HashMap::default());
+ }
+
+ if let Some(data) = get_jupyter_display(session, evaluate_result).await? {
+ return Ok(data);
+ }
+
+ let response = session
+ .call_function_on_args(
+ format!(
+ r#"function (object) {{
+ try {{
+ return {0}.inspectArgs(["%o", object], {{ colors: !{0}.noColor }});
+ }} catch (err) {{
+ return {0}.inspectArgs(["%o", err]);
+ }}
+ }}"#,
+ *repl::REPL_INTERNALS_NAME
+ ),
+ &[evaluate_result.clone()],
+ )
+ .await?;
+ let mut data = HashMap::default();
+ if let Some(value) = response.result.value {
+ data.insert("text/plain".to_string(), value);
+ }
+
+ Ok(data)
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+fn get_expr_from_line_at_pos(line: &str, cursor_pos: usize) -> &str {
+ let start = line[..cursor_pos].rfind(is_word_boundary).unwrap_or(0);
+ let end = line[cursor_pos..]
+ .rfind(is_word_boundary)
+ .map(|i| cursor_pos + i)
+ .unwrap_or(cursor_pos);
+
+ let word = &line[start..end];
+ let word = word.strip_prefix(is_word_boundary).unwrap_or(word);
+ let word = word.strip_suffix(is_word_boundary).unwrap_or(word);
+
+ word
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+fn is_word_boundary(c: char) -> bool {
+ if matches!(c, '.' | '_' | '$') {
+ false
+ } else {
+ char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c)
+ }
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+async fn get_global_lexical_scope_names(
+ session: &mut repl::ReplSession,
+) -> Vec<String> {
+ let evaluate_response = session
+ .post_message_with_event_loop(
+ "Runtime.globalLexicalScopeNames",
+ Some(cdp::GlobalLexicalScopeNamesArgs {
+ execution_context_id: Some(session.context_id),
+ }),
+ )
+ .await
+ .unwrap();
+ let evaluate_response: cdp::GlobalLexicalScopeNamesResponse =
+ serde_json::from_value(evaluate_response).unwrap();
+ evaluate_response.names
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+async fn get_expression_property_names(
+ session: &mut repl::ReplSession,
+ expr: &str,
+) -> Vec<String> {
+ // try to get the properties from the expression
+ if let Some(properties) = get_object_expr_properties(session, expr).await {
+ return properties;
+ }
+
+ // otherwise fall back to the prototype
+ let expr_type = get_expression_type(session, expr).await;
+ let object_expr = match expr_type.as_deref() {
+ // possibilities: https://chromedevtools.github.io/devtools-protocol/v8/Runtime/#type-RemoteObject
+ Some("object") => "Object.prototype",
+ Some("function") => "Function.prototype",
+ Some("string") => "String.prototype",
+ Some("boolean") => "Boolean.prototype",
+ Some("bigint") => "BigInt.prototype",
+ Some("number") => "Number.prototype",
+ _ => return Vec::new(), // undefined, symbol, and unhandled
+ };
+
+ get_object_expr_properties(session, object_expr)
+ .await
+ .unwrap_or_default()
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+async fn get_expression_type(
+ session: &mut repl::ReplSession,
+ expr: &str,
+) -> Option<String> {
+ evaluate_expression(session, expr)
+ .await
+ .map(|res| res.result.kind)
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+async fn get_object_expr_properties(
+ session: &mut repl::ReplSession,
+ object_expr: &str,
+) -> Option<Vec<String>> {
+ let evaluate_result = evaluate_expression(session, object_expr).await?;
+ let object_id = evaluate_result.result.object_id?;
+
+ let get_properties_response = session
+ .post_message_with_event_loop(
+ "Runtime.getProperties",
+ Some(cdp::GetPropertiesArgs {
+ object_id,
+ own_properties: None,
+ accessor_properties_only: None,
+ generate_preview: None,
+ non_indexed_properties_only: Some(true),
+ }),
+ )
+ .await
+ .ok()?;
+ let get_properties_response: cdp::GetPropertiesResponse =
+ serde_json::from_value(get_properties_response).ok()?;
+ Some(
+ get_properties_response
+ .result
+ .into_iter()
+ .map(|prop| prop.name)
+ .collect(),
+ )
+}
+
+// TODO(bartlomieju): dedup with repl::editor
+async fn evaluate_expression(
+ session: &mut repl::ReplSession,
+ expr: &str,
+) -> Option<cdp::EvaluateResponse> {
+ let evaluate_response = session
+ .post_message_with_event_loop(
+ "Runtime.evaluate",
+ Some(cdp::EvaluateArgs {
+ expression: expr.to_string(),
+ object_group: None,
+ include_command_line_api: None,
+ silent: None,
+ context_id: Some(session.context_id),
+ return_by_value: None,
+ generate_preview: None,
+ user_gesture: None,
+ await_promise: None,
+ throw_on_side_effect: Some(true),
+ timeout: Some(200),
+ disable_breaks: None,
+ repl_mode: None,
+ allow_unsafe_eval_blocked_by_csp: None,
+ unique_context_id: None,
+ }),
+ )
+ .await
+ .ok()?;
+ let evaluate_response: cdp::EvaluateResponse =
+ serde_json::from_value(evaluate_response).ok()?;
+
+ if evaluate_response.exception_details.is_some() {
+ None
+ } else {
+ Some(evaluate_response)
+ }
+}
diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs
index c4a8306ab..13a37addd 100644
--- a/cli/tools/mod.rs
+++ b/cli/tools/mod.rs
@@ -10,6 +10,7 @@ pub mod fmt;
pub mod info;
pub mod init;
pub mod installer;
+pub mod jupyter;
pub mod lint;
pub mod repl;
pub mod run;
diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs
index fb0891fa6..a1e741dfd 100644
--- a/cli/tools/repl/mod.rs
+++ b/cli/tools/repl/mod.rs
@@ -13,7 +13,7 @@ use deno_runtime::permissions::Permissions;
use deno_runtime::permissions::PermissionsContainer;
use rustyline::error::ReadlineError;
-mod cdp;
+pub(crate) mod cdp;
mod channel;
mod editor;
mod session;
@@ -24,8 +24,9 @@ use channel::RustylineSyncMessageHandler;
use channel::RustylineSyncResponse;
use editor::EditorHelper;
use editor::ReplEditor;
-use session::EvaluationOutput;
-use session::ReplSession;
+pub use session::EvaluationOutput;
+pub use session::ReplSession;
+pub use session::REPL_INTERNALS_NAME;
#[allow(clippy::await_holding_refcell_ref)]
async fn read_line_and_poll(
diff --git a/cli/tools/repl/session.rs b/cli/tools/repl/session.rs
index d89cc95c3..a1b602b4b 100644
--- a/cli/tools/repl/session.rs
+++ b/cli/tools/repl/session.rs
@@ -116,9 +116,10 @@ pub fn result_to_evaluation_output(
}
}
-struct TsEvaluateResponse {
- ts_code: String,
- value: cdp::EvaluateResponse,
+#[derive(Debug)]
+pub struct TsEvaluateResponse {
+ pub ts_code: String,
+ pub value: cdp::EvaluateResponse,
}
pub struct ReplSession {
@@ -305,7 +306,7 @@ impl ReplSession {
result_to_evaluation_output(result)
}
- async fn evaluate_line_with_object_wrapping(
+ pub async fn evaluate_line_with_object_wrapping(
&mut self,
line: &str,
) -> Result<TsEvaluateResponse, AnyError> {
@@ -395,29 +396,24 @@ impl ReplSession {
Ok(())
}
- pub async fn get_eval_value(
+ pub async fn call_function_on_args(
&mut self,
- evaluate_result: &cdp::RemoteObject,
- ) -> Result<String, AnyError> {
- // TODO(caspervonb) we should investigate using previews here but to keep things
- // consistent with the previous implementation we just get the preview result from
- // Deno.inspectArgs.
+ function_declaration: String,
+ args: &[cdp::RemoteObject],
+ ) -> Result<cdp::CallFunctionOnResponse, AnyError> {
+ let arguments: Option<Vec<cdp::CallArgument>> = if args.is_empty() {
+ None
+ } else {
+ Some(args.iter().map(|a| a.into()).collect())
+ };
+
let inspect_response = self
.post_message_with_event_loop(
"Runtime.callFunctionOn",
Some(cdp::CallFunctionOnArgs {
- function_declaration: format!(
- r#"function (object) {{
- try {{
- return {0}.inspectArgs(["%o", object], {{ colors: !{0}.noColor }});
- }} catch (err) {{
- return {0}.inspectArgs(["%o", err]);
- }}
- }}"#,
- *REPL_INTERNALS_NAME
- ),
+ function_declaration,
object_id: None,
- arguments: Some(vec![evaluate_result.into()]),
+ arguments,
silent: None,
return_by_value: None,
generate_preview: None,
@@ -432,6 +428,31 @@ impl ReplSession {
let response: cdp::CallFunctionOnResponse =
serde_json::from_value(inspect_response)?;
+ Ok(response)
+ }
+
+ pub async fn get_eval_value(
+ &mut self,
+ evaluate_result: &cdp::RemoteObject,
+ ) -> Result<String, AnyError> {
+ // TODO(caspervonb) we should investigate using previews here but to keep things
+ // consistent with the previous implementation we just get the preview result from
+ // Deno.inspectArgs.
+ let response = self
+ .call_function_on_args(
+ format!(
+ r#"function (object) {{
+ try {{
+ return {0}.inspectArgs(["%o", object], {{ colors: !{0}.noColor }});
+ }} catch (err) {{
+ return {0}.inspectArgs(["%o", err]);
+ }}
+ }}"#,
+ *REPL_INTERNALS_NAME
+ ),
+ &[evaluate_result.clone()],
+ )
+ .await?;
let value = response.result.value.unwrap();
let s = value.as_str().unwrap();