diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 96cd0bbfe7..235743bda2 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -206,7 +206,7 @@ pub struct ReplFlags { pub is_default_command: bool, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct RunFlags { pub script: String, pub watch: Option, @@ -311,6 +311,10 @@ impl DenoSubcommand { pub fn is_run(&self) -> bool { matches!(self, Self::Run(_)) } + + pub fn is_test_or_jupyter(&self) -> bool { + matches!(self, Self::Test(_) | Self::Jupyter(_)) + } } impl Default for DenoSubcommand { diff --git a/cli/build.rs b/cli/build.rs index 4fe3a5d170..6260c1c61b 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -332,7 +332,6 @@ deno_core::extension!( esm = [ dir "js", "40_testing.js", - "40_jupyter.js", "99_main.js" ], customizer = |ext: &mut deno_core::Extension| { diff --git a/cli/factory.rs b/cli/factory.rs index 4e7138938d..5ea3e2d0bf 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -634,6 +634,7 @@ impl CliFactory { Ok(CliMainWorkerFactory::new( StorageKeyResolver::from_options(&self.options), + self.options.sub_command().clone(), npm_resolver.clone(), node_resolver.clone(), self.blob_store().clone(), diff --git a/cli/js/40_jupyter.js b/cli/js/40_jupyter.js index c4b27ad0b6..a0a6374729 100644 --- a/cli/js/40_jupyter.js +++ b/cli/js/40_jupyter.js @@ -1,4 +1,5 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file /* * @module mod @@ -35,184 +36,215 @@ * }, { raw: true }); * ``` */ +{ + const internals = Deno[Deno.internal]; + const core = internals.core; -const core = globalThis.Deno.core; + const $display = Symbol.for("Jupyter.display"); -const internals = globalThis.__bootstrap.internals; + /** Escape copied from https://deno.land/std@0.192.0/html/entities.ts */ + const rawToEntityEntries = [ + ["&", "&"], + ["<", "<"], + [">", ">"], + ['"', """], + ["'", "'"], + ]; -const $display = Symbol.for("Jupyter.display"); + const rawToEntity = new Map(rawToEntityEntries); -/** Escape copied from https://deno.land/std@0.192.0/html/entities.ts */ -const rawToEntityEntries = [ - ["&", "&"], - ["<", "<"], - [">", ">"], - ['"', """], - ["'", "'"], -]; + const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g"); -const rawToEntity = new Map(rawToEntityEntries); - -const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g"); - -function escapeHTML(str) { - return str.replaceAll( - rawRe, - (m) => rawToEntity.has(m) ? rawToEntity.get(m) : m, - ); -} - -/** Duck typing our way to common visualization and tabular libraries */ -/** Vegalite */ -function isVegaLike(obj) { - return obj !== null && typeof obj === "object" && "toSpec" in obj; -} -function extractVega(obj) { - const spec = obj.toSpec(); - if (!("$schema" in spec)) { - return null; + function escapeHTML(str) { + return str.replaceAll( + rawRe, + (m) => rawToEntity.has(m) ? rawToEntity.get(m) : m, + ); } - if (typeof spec !== "object") { - return null; - } - let mediaType = "application/vnd.vega.v5+json"; - if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") { - mediaType = "application/vnd.vegalite.v4+json"; - } else if ( - spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json" - ) { - mediaType = "application/vnd.vegalite.v5+json"; - } - return { - [mediaType]: spec, - }; -} -/** Polars */ -function isDataFrameLike(obj) { - const isObject = obj !== null && typeof obj === "object"; - if (!isObject) { - return false; - } - const df = obj; - return df.schema !== void 0 && typeof df.schema === "object" && - df.head !== void 0 && typeof df.head === "function" && - df.toRecords !== void 0 && typeof df.toRecords === "function"; -} -/** - * Map Polars DataType to JSON Schema data types. - * @param dataType - The Polars DataType. - * @returns The corresponding JSON Schema data type. - */ -function mapPolarsTypeToJSONSchema(colType) { - const typeMapping = { - Null: "null", - Bool: "boolean", - Int8: "integer", - Int16: "integer", - Int32: "integer", - Int64: "integer", - UInt8: "integer", - UInt16: "integer", - UInt32: "integer", - UInt64: "integer", - Float32: "number", - Float64: "number", - Date: "string", - Datetime: "string", - Utf8: "string", - Categorical: "string", - List: "array", - Struct: "object", - }; - // These colTypes are weird. When you console.dir or console.log them - // they show a `DataType` field, however you can't access it directly until you - // convert it to JSON - const dataType = colType.toJSON()["DataType"]; - return typeMapping[dataType] || "string"; -} -function extractDataFrame(df) { - const fields = []; - const schema = { - fields, - }; - let data = []; - // Convert DataFrame schema to Tabular DataResource schema - for (const [colName, colType] of Object.entries(df.schema)) { - const dataType = mapPolarsTypeToJSONSchema(colType); - schema.fields.push({ - name: colName, - type: dataType, - }); + /** Duck typing our way to common visualization and tabular libraries */ + /** Vegalite */ + function isVegaLike(obj) { + return obj !== null && typeof obj === "object" && "toSpec" in obj; + } + function extractVega(obj) { + const spec = obj.toSpec(); + if (!("$schema" in spec)) { + return null; + } + if (typeof spec !== "object") { + return null; + } + let mediaType = "application/vnd.vega.v5+json"; + if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") { + mediaType = "application/vnd.vegalite.v4+json"; + } else if ( + spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json" + ) { + mediaType = "application/vnd.vegalite.v5+json"; + } + return { + [mediaType]: spec, + }; + } + /** Polars */ + function isDataFrameLike(obj) { + const isObject = obj !== null && typeof obj === "object"; + if (!isObject) { + return false; + } + const df = obj; + return df.schema !== void 0 && typeof df.schema === "object" && + df.head !== void 0 && typeof df.head === "function" && + df.toRecords !== void 0 && typeof df.toRecords === "function"; + } + /** + * Map Polars DataType to JSON Schema data types. + * @param dataType - The Polars DataType. + * @returns The corresponding JSON Schema data type. + */ + function mapPolarsTypeToJSONSchema(colType) { + const typeMapping = { + Null: "null", + Bool: "boolean", + Int8: "integer", + Int16: "integer", + Int32: "integer", + Int64: "integer", + UInt8: "integer", + UInt16: "integer", + UInt32: "integer", + UInt64: "integer", + Float32: "number", + Float64: "number", + Date: "string", + Datetime: "string", + Utf8: "string", + Categorical: "string", + List: "array", + Struct: "object", + }; + // These colTypes are weird. When you console.dir or console.log them + // they show a `DataType` field, however you can't access it directly until you + // convert it to JSON + const dataType = colType.toJSON()["DataType"]; + return typeMapping[dataType] || "string"; } - // Convert DataFrame data to row-oriented JSON - // - // TODO(rgbkrk): Determine how to get the polars format max rows - // Since pl.setTblRows just sets env var POLARS_FMT_MAX_ROWS, - // we probably just have to pick a number for now. - // - data = df.head(50).toRecords(); - let htmlTable = ""; - htmlTable += ""; - schema.fields.forEach((field) => { - htmlTable += ``; - }); - htmlTable += ""; - htmlTable += ""; - df.head(10).toRecords().forEach((row) => { - htmlTable += ""; + function extractDataFrame(df) { + const fields = []; + const schema = { + fields, + }; + let data = []; + // Convert DataFrame schema to Tabular DataResource schema + for (const [colName, colType] of Object.entries(df.schema)) { + const dataType = mapPolarsTypeToJSONSchema(colType); + schema.fields.push({ + name: colName, + type: dataType, + }); + } + // Convert DataFrame data to row-oriented JSON + // + // TODO(rgbkrk): Determine how to get the polars format max rows + // Since pl.setTblRows just sets env var POLARS_FMT_MAX_ROWS, + // we probably just have to pick a number for now. + // + + data = df.head(50).toRecords(); + let htmlTable = "
${escapeHTML(String(field.name))}
"; + htmlTable += ""; schema.fields.forEach((field) => { - htmlTable += ``; + htmlTable += ``; }); - htmlTable += ""; - }); - htmlTable += "
${escapeHTML(String(row[field.name]))}${escapeHTML(String(field.name))}
"; - return { - "application/vnd.dataresource+json": { data, schema }, - "text/html": htmlTable, - }; -} - -/** Canvas */ -function isCanvasLike(obj) { - return obj !== null && typeof obj === "object" && "toDataURL" in obj; -} - -/** Possible HTML and SVG Elements */ -function isSVGElementLike(obj) { - return obj !== null && typeof obj === "object" && "outerHTML" in obj && - typeof obj.outerHTML === "string" && obj.outerHTML.startsWith(" obj, - }; -} - -/** - * Format an object for displaying in Deno - * - * @param obj - The object to be displayed - * @returns MediaBundle - */ -async function format(obj) { - if (hasDisplaySymbol(obj)) { - return await obj[$display](); + htmlTable += ""; + htmlTable += ""; + df.head(10).toRecords().forEach((row) => { + htmlTable += ""; + schema.fields.forEach((field) => { + htmlTable += `${escapeHTML(String(row[field.name]))}`; + }); + htmlTable += ""; + }); + htmlTable += ""; + return { + "application/vnd.dataresource+json": { data, schema }, + "text/html": htmlTable, + }; } - if (typeof obj !== "object") { + + /** Canvas */ + function isCanvasLike(obj) { + return obj !== null && typeof obj === "object" && "toDataURL" in obj; + } + + /** Possible HTML and SVG Elements */ + function isSVGElementLike(obj) { + return obj !== null && typeof obj === "object" && "outerHTML" in obj && + typeof obj.outerHTML === "string" && obj.outerHTML.startsWith(" obj, + }; + } + + /** + * Format an object for displaying in Deno + * + * @param obj - The object to be displayed + * @returns MediaBundle + */ + async function format(obj) { + if (hasDisplaySymbol(obj)) { + return await obj[$display](); + } + if (typeof obj !== "object") { + return { + "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { + colors: !Deno.noColor, + }), + }; + } + + if (isCanvasLike(obj)) { + const dataURL = obj.toDataURL(); + const parts = dataURL.split(","); + const mime = parts[0].split(":")[1].split(";")[0]; + const data = parts[1]; + return { + [mime]: data, + }; + } + if (isVegaLike(obj)) { + return extractVega(obj); + } + if (isDataFrameLike(obj)) { + return extractDataFrame(obj); + } + if (isSVGElementLike(obj)) { + return { + "image/svg+xml": obj.outerHTML, + }; + } + if (isHTMLElementLike(obj)) { + return { + "text/html": obj.outerHTML, + }; + } return { "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { colors: !Deno.noColor, @@ -220,211 +252,180 @@ async function format(obj) { }; } - if (isCanvasLike(obj)) { - const dataURL = obj.toDataURL(); - const parts = dataURL.split(","); - const mime = parts[0].split(":")[1].split(";")[0]; - const data = parts[1]; - return { - [mime]: data, + /** + * This function creates a tagged template function for a given media type. + * The tagged template function takes a template string and returns a displayable object. + * + * @param mediatype - The media type for the tagged template function. + * @returns A function that takes a template string and returns a displayable object. + */ + function createTaggedTemplateDisplayable(mediatype) { + return (strings, ...values) => { + const payload = strings.reduce( + (acc, string, i) => + acc + string + (values[i] !== undefined ? values[i] : ""), + "", + ); + return makeDisplayable({ [mediatype]: payload }); }; } - if (isVegaLike(obj)) { - return extractVega(obj); - } - if (isDataFrameLike(obj)) { - return extractDataFrame(obj); - } - if (isSVGElementLike(obj)) { - return { - "image/svg+xml": obj.outerHTML, - }; - } - if (isHTMLElementLike(obj)) { - return { - "text/html": obj.outerHTML, - }; - } - return { - "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { - colors: !Deno.noColor, - }), - }; -} - -/** - * This function creates a tagged template function for a given media type. - * The tagged template function takes a template string and returns a displayable object. - * - * @param mediatype - The media type for the tagged template function. - * @returns A function that takes a template string and returns a displayable object. - */ -function createTaggedTemplateDisplayable(mediatype) { - return (strings, ...values) => { - const payload = strings.reduce( - (acc, string, i) => - acc + string + (values[i] !== undefined ? values[i] : ""), - "", - ); - return makeDisplayable({ [mediatype]: payload }); - }; -} - -/** - * Show Markdown in Jupyter frontends with a tagged template function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * @example - * Create a Markdown view. - * - * ```typescript - * md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32) - * - * * TypeScript ${Deno.version.typescript} - * * V8 ${Deno.version.v8} - * * Deno ${Deno.version.deno} - * - * Interactive compute with Jupyter _built into Deno_! - * ` - * ``` - */ -const md = createTaggedTemplateDisplayable("text/markdown"); - -/** - * Show HTML in Jupyter frontends with a tagged template function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * @example - * Create an HTML view. - * ```typescript - * html`

Hello, world!

` - * ``` - */ -const html = createTaggedTemplateDisplayable("text/html"); -/** - * SVG Tagged Template Function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * Example usage: - * - * svg` - * - * ` - */ -const svg = createTaggedTemplateDisplayable("image/svg+xml"); - -function isMediaBundle(obj) { - if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { - return false; - } - for (const key in obj) { - if (typeof key !== "string") { - return false; - } - } - return true; -} - -async function formatInner(obj, raw) { - if (raw && isMediaBundle(obj)) { - return obj; - } else { - return await format(obj); - } -} - -internals.jupyter = { formatInner }; - -function enableJupyter() { - const { - op_jupyter_broadcast, - } = core.ensureFastOps(); - - async function broadcast( - msgType, - content, - { metadata = {}, buffers = [] } = {}, - ) { - await op_jupyter_broadcast(msgType, content, metadata, buffers); - } - - async function broadcastResult(executionCount, result) { - try { - if (result === undefined) { - return; - } - - const data = await format(result); - await broadcast("execute_result", { - execution_count: executionCount, - data, - metadata: {}, - }); - } catch (err) { - if (err instanceof Error) { - const stack = err.stack || ""; - await broadcast("error", { - ename: err.name, - evalue: err.message, - traceback: stack.split("\n"), - }); - } else if (typeof err == "string") { - await broadcast("error", { - ename: "Error", - evalue: err, - traceback: [], - }); - } else { - await broadcast("error", { - ename: "Error", - evalue: - "An error occurred while formatting a result, but it could not be identified", - traceback: [], - }); - } - } - } - - internals.jupyter.broadcastResult = broadcastResult; /** - * Display function for Jupyter Deno Kernel. - * Mimics the behavior of IPython's `display(obj, raw=True)` function to allow - * asynchronous displaying of objects in Jupyter. + * Show Markdown in Jupyter frontends with a tagged template function. * - * @param obj - The object to be displayed - * @param options - Display options + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * @example + * Create a Markdown view. + * + * ```typescript + * md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32) + * + * * TypeScript ${Deno.version.typescript} + * * V8 ${Deno.version.v8} + * * Deno ${Deno.version.deno} + * + * Interactive compute with Jupyter _built into Deno_! + * ` + * ``` */ - async function display(obj, options = { raw: false, update: false }) { - const bundle = await formatInner(obj, options.raw); - let messageType = "display_data"; - if (options.update) { - messageType = "update_display_data"; + const md = createTaggedTemplateDisplayable("text/markdown"); + + /** + * Show HTML in Jupyter frontends with a tagged template function. + * + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * @example + * Create an HTML view. + * ```typescript + * html`

Hello, world!

` + * ``` + */ + const html = createTaggedTemplateDisplayable("text/html"); + /** + * SVG Tagged Template Function. + * + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * Example usage: + * + * svg` + * + * ` + */ + const svg = createTaggedTemplateDisplayable("image/svg+xml"); + + function isMediaBundle(obj) { + if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { + return false; } - let transient = {}; - if (options.display_id) { - transient = { display_id: options.display_id }; + for (const key in obj) { + if (typeof key !== "string") { + return false; + } } - await broadcast(messageType, { - data: bundle, - metadata: {}, - transient, - }); - return; + return true; } - globalThis.Deno.jupyter = { - broadcast, - display, - format, - md, - html, - svg, - $display, - }; -} + async function formatInner(obj, raw) { + if (raw && isMediaBundle(obj)) { + return obj; + } else { + return await format(obj); + } + } -internals.enableJupyter = enableJupyter; + internals.jupyter = { formatInner }; + + function enableJupyter() { + const { + op_jupyter_broadcast, + } = core.ensureFastOps(); + + async function broadcast( + msgType, + content, + { metadata = {}, buffers = [] } = {}, + ) { + await op_jupyter_broadcast(msgType, content, metadata, buffers); + } + + async function broadcastResult(executionCount, result) { + try { + if (result === undefined) { + return; + } + + const data = await format(result); + await broadcast("execute_result", { + execution_count: executionCount, + data, + metadata: {}, + }); + } catch (err) { + if (err instanceof Error) { + const stack = err.stack || ""; + await broadcast("error", { + ename: err.name, + evalue: err.message, + traceback: stack.split("\n"), + }); + } else if (typeof err == "string") { + await broadcast("error", { + ename: "Error", + evalue: err, + traceback: [], + }); + } else { + await broadcast("error", { + ename: "Error", + evalue: + "An error occurred while formatting a result, but it could not be identified", + traceback: [], + }); + } + } + } + + internals.jupyter.broadcastResult = broadcastResult; + + /** + * Display function for Jupyter Deno Kernel. + * Mimics the behavior of IPython's `display(obj, raw=True)` function to allow + * asynchronous displaying of objects in Jupyter. + * + * @param obj - The object to be displayed + * @param options - Display options + */ + async function display(obj, options = { raw: false, update: false }) { + const bundle = await formatInner(obj, options.raw); + let messageType = "display_data"; + if (options.update) { + messageType = "update_display_data"; + } + let transient = {}; + if (options.display_id) { + transient = { display_id: options.display_id }; + } + await broadcast(messageType, { + data: bundle, + metadata: {}, + transient, + }); + return; + } + + globalThis.Deno.jupyter = { + broadcast, + display, + format, + md, + html, + svg, + $display, + }; + } + + internals.enableJupyter = enableJupyter; +} diff --git a/cli/js/99_main.js b/cli/js/99_main.js index 37342f6cd6..dc9d74fb06 100644 --- a/cli/js/99_main.js +++ b/cli/js/99_main.js @@ -1,4 +1,3 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import "ext:cli/40_testing.js"; -import "ext:cli/40_jupyter.js"; import "ext:cli/runtime/js/99_main.js"; diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index 1021b80ee4..f34e5f39cb 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -22,7 +22,6 @@ deno_core::extension!(cli, esm = [ dir "js", "40_testing.js", - "40_jupyter.js", "99_main.js" ], customizer = |ext: &mut deno_core::Extension| { diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 56d4b1974a..13123a8d63 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -438,6 +438,7 @@ pub async fn run( }); let worker_factory = CliMainWorkerFactory::new( StorageKeyResolver::empty(), + crate::args::DenoSubcommand::Run(Default::default()), npm_resolver, node_resolver, Default::default(), diff --git a/cli/worker.rs b/cli/worker.rs index 957055d0a0..2ae7bade4f 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -46,6 +46,7 @@ use deno_semver::package::PackageReqReference; use tokio::select; use crate::args::package_json::PackageJsonDeps; +use crate::args::DenoSubcommand; use crate::args::StorageKeyResolver; use crate::emit::Emitter; use crate::errors; @@ -107,6 +108,7 @@ pub struct CliMainWorkerOptions { struct SharedWorkerState { options: CliMainWorkerOptions, + subcommand: DenoSubcommand, storage_key_resolver: StorageKeyResolver, npm_resolver: Arc, node_resolver: Arc, @@ -372,6 +374,7 @@ impl CliMainWorkerFactory { #[allow(clippy::too_many_arguments)] pub fn new( storage_key_resolver: StorageKeyResolver, + subcommand: DenoSubcommand, npm_resolver: Arc, node_resolver: Arc, blob_store: Arc, @@ -388,6 +391,7 @@ impl CliMainWorkerFactory { Self { shared: Arc::new(SharedWorkerState { options, + subcommand, storage_key_resolver, npm_resolver, node_resolver, @@ -600,12 +604,19 @@ impl CliMainWorkerFactory { skip_op_registration: shared.options.skip_op_registration, }; - let worker = MainWorker::bootstrap_from_options( + let mut worker = MainWorker::bootstrap_from_options( main_module.clone(), permissions, options, ); + if self.shared.subcommand.is_test_or_jupyter() { + worker.js_runtime.execute_script_static( + "40_jupyter.js", + include_str!("js/40_jupyter.js"), + )?; + } + Ok(CliMainWorker { main_module, is_main_cjs,