diff --git a/cli/args/flags.rs b/cli/args/flags.rs index d57f78aff3..71398d355c 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -22,6 +22,7 @@ use log::Level; use std::env; use std::ffi::OsString; use std::net::SocketAddr; +use std::num::NonZeroU16; use std::num::NonZeroU32; use std::num::NonZeroU8; use std::num::NonZeroUsize; @@ -272,6 +273,26 @@ impl RunFlags { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ServeFlags { + pub script: String, + pub watch: Option, + pub port: NonZeroU16, + pub host: String, +} + +impl ServeFlags { + #[cfg(test)] + pub fn new_default(script: String, port: u16, host: &str) -> Self { + Self { + script, + watch: None, + port: NonZeroU16::new(port).unwrap(), + host: host.to_owned(), + } + } +} + #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct WatchFlags { pub hmr: bool, @@ -366,6 +387,7 @@ pub enum DenoSubcommand { Lint(LintFlags), Repl(ReplFlags), Run(RunFlags), + Serve(ServeFlags), Task(TaskFlags), Test(TestFlags), Types, @@ -776,7 +798,7 @@ impl Flags { use DenoSubcommand::*; match &self.subcommand { - Run(RunFlags { script, .. }) => { + Run(RunFlags { script, .. }) | Serve(ServeFlags { script, .. }) => { let module_specifier = resolve_url_or_path(script, current_dir).ok()?; if module_specifier.scheme() == "file" { let p = module_specifier @@ -1063,6 +1085,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { "lsp" => lsp_parse(&mut flags, &mut m), "repl" => repl_parse(&mut flags, &mut m), "run" => run_parse(&mut flags, &mut m, app)?, + "serve" => serve_parse(&mut flags, &mut m, app)?, "task" => task_parse(&mut flags, &mut m), "test" => test_parse(&mut flags, &mut m), "types" => types_parse(&mut flags, &mut m), @@ -1198,6 +1221,7 @@ fn clap_root() -> Command { .global(true), ) .subcommand(run_subcommand()) + .subcommand(serve_subcommand()) .defer(|cmd| { cmd .subcommand(add_subcommand()) @@ -2265,6 +2289,59 @@ Specifying the filename '-' to read the file from stdin. ) } +fn serve_host_validator(host: &str) -> Result { + if Url::parse(&format!("internal://{host}:9999")).is_ok() { + Ok(host.to_owned()) + } else { + Err(format!("Bad serve host: {host}")) + } +} + +fn serve_subcommand() -> Command { + runtime_args(Command::new("serve"), true, true) + .arg( + Arg::new("port") + .long("port") + .help("The TCP port to serve on, defaulting to 8000.") + .value_parser(value_parser!(NonZeroU16)), + ) + .arg( + Arg::new("host") + .long("host") + .help("The TCP address to serve on, defaulting to 0.0.0.0 (all interfaces).") + .value_parser(serve_host_validator), + ) + .arg(check_arg(false)) + .arg(watch_arg(true)) + .arg(watch_exclude_arg()) + .arg(hmr_arg(true)) + .arg(no_clear_screen_arg()) + .arg(executable_ext_arg()) + .arg( + script_arg() + .required_unless_present("v8-flags") + .trailing_var_arg(true), + ) + .arg(env_file_arg()) + .arg(no_code_cache_arg()) + .about("Run a server") + .long_about("Run a server defined in a main module + +The serve command uses the default exports of the main module to determine which +servers to start. + +See https://docs.deno.com/runtime/manual/tools/serve for +more detailed information. + +Start a server defined in server.ts: + + deno serve server.ts + +Start a server defined in server.ts, watching for changes and running on port 5050: + + deno serve --watch --port 5050 server.ts") +} + fn task_subcommand() -> Command { Command::new("task") .about("Run a task defined in the configuration file") @@ -3863,6 +3940,63 @@ fn run_parse( Ok(()) } +fn serve_parse( + flags: &mut Flags, + matches: &mut ArgMatches, + app: Command, +) -> clap::error::Result<()> { + // deno serve implies --allow-net=host:port + let port = matches + .remove_one::("port") + .unwrap_or(NonZeroU16::new(8000).unwrap()); + let host = matches + .remove_one::("host") + .unwrap_or_else(|| "0.0.0.0".to_owned()); + + runtime_args_parse(flags, matches, true, true); + // If the user didn't pass --allow-net, add this port to the network + // allowlist. If the host is 0.0.0.0, we add :{port} and allow the same network perms + // as if it was passed to --allow-net directly. + let allowed = flags_net::parse(vec![if host == "0.0.0.0" { + format!(":{port}") + } else { + format!("{host}:{port}") + }])?; + match &mut flags.allow_net { + None => flags.allow_net = Some(allowed), + Some(v) => { + if !v.is_empty() { + v.extend(allowed); + } + } + } + flags.code_cache_enabled = !matches.get_flag("no-code-cache"); + + let mut script_arg = + matches.remove_many::("script_arg").ok_or_else(|| { + let mut app = app; + let subcommand = &mut app.find_subcommand_mut("serve").unwrap(); + subcommand.error( + clap::error::ErrorKind::MissingRequiredArgument, + "[SCRIPT_ARG] may only be omitted with --v8-flags=--help", + ) + })?; + + let script = script_arg.next().unwrap(); + flags.argv.extend(script_arg); + + ext_arg_parse(flags, matches); + + flags.subcommand = DenoSubcommand::Serve(ServeFlags { + script, + watch: watch_arg_parse_with_paths(matches), + port, + host, + }); + + Ok(()) +} + fn task_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.config_flag = matches .remove_one::("config") @@ -4858,7 +4992,7 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Run(RunFlags::new_default( - "script.ts".to_string() + "script.ts".to_string(), )), v8_flags: svec!["--expose-gc", "--gc-stats=1"], code_cache_enabled: true, @@ -4873,6 +5007,115 @@ mod tests { .contains("[SCRIPT_ARG] may only be omitted with --v8-flags=--help")); } + #[test] + fn serve_flags() { + let r = flags_from_vec(svec!["deno", "serve", "main.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Serve(ServeFlags::new_default( + "main.ts".to_string(), + 8000, + "0.0.0.0" + )), + allow_net: Some(vec![ + "0.0.0.0:8000".to_string(), + "127.0.0.1:8000".to_string(), + "localhost:8000".to_string() + ]), + code_cache_enabled: true, + ..Flags::default() + } + ); + let r = flags_from_vec(svec!["deno", "serve", "--port", "5000", "main.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Serve(ServeFlags::new_default( + "main.ts".to_string(), + 5000, + "0.0.0.0" + )), + allow_net: Some(vec![ + "0.0.0.0:5000".to_string(), + "127.0.0.1:5000".to_string(), + "localhost:5000".to_string() + ]), + code_cache_enabled: true, + ..Flags::default() + } + ); + let r = flags_from_vec(svec![ + "deno", + "serve", + "--port", + "5000", + "--allow-net=example.com", + "main.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Serve(ServeFlags::new_default( + "main.ts".to_string(), + 5000, + "0.0.0.0" + )), + allow_net: Some(vec![ + "example.com".to_string(), + "0.0.0.0:5000".to_string(), + "127.0.0.1:5000".to_string(), + "localhost:5000".to_string() + ]), + code_cache_enabled: true, + ..Flags::default() + } + ); + let r = flags_from_vec(svec![ + "deno", + "serve", + "--port", + "5000", + "--allow-net", + "main.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Serve(ServeFlags::new_default( + "main.ts".to_string(), + 5000, + "0.0.0.0" + )), + allow_net: Some(vec![]), + code_cache_enabled: true, + ..Flags::default() + } + ); + let r = flags_from_vec(svec![ + "deno", + "serve", + "--port", + "5000", + "--host", + "example.com", + "main.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Serve(ServeFlags::new_default( + "main.ts".to_string(), + 5000, + "example.com" + )), + allow_net: Some(vec!["example.com:5000".to_owned()]), + code_cache_enabled: true, + ..Flags::default() + } + ); + } + #[test] fn has_permission() { let r = flags_from_vec(svec!["deno", "run", "--allow-read", "x.ts"]); diff --git a/cli/args/mod.rs b/cli/args/mod.rs index aa3622d096..bb0ef1abac 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -61,6 +61,7 @@ use std::env; use std::io::BufReader; use std::io::Cursor; use std::net::SocketAddr; +use std::num::NonZeroU16; use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; @@ -1022,6 +1023,22 @@ impl CliOptions { } } + pub fn serve_port(&self) -> Option { + if let DenoSubcommand::Serve(flags) = self.sub_command() { + Some(flags.port) + } else { + None + } + } + + pub fn serve_host(&self) -> Option { + if let DenoSubcommand::Serve(flags) = self.sub_command() { + Some(flags.host.clone()) + } else { + None + } + } + pub fn enable_future_features(&self) -> bool { *DENO_FUTURE } @@ -1062,6 +1079,10 @@ impl CliOptions { .map_err(AnyError::from) } } + DenoSubcommand::Serve(run_flags) => { + resolve_url_or_path(&run_flags.script, self.initial_cwd()) + .map_err(AnyError::from) + } _ => { bail!("No main module.") } diff --git a/cli/factory.rs b/cli/factory.rs index 1a0584eeac..bfaf96f39c 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -219,6 +219,7 @@ impl CliFactory { // Warm up the caches we know we'll likely need based on the CLI mode match self.options.sub_command() { DenoSubcommand::Run(_) + | DenoSubcommand::Serve(_) | DenoSubcommand::Bench(_) | DenoSubcommand::Test(_) | DenoSubcommand::Check(_) => { @@ -816,6 +817,8 @@ impl CliFactory { self.feature_checker().clone(), self.create_cli_main_worker_options()?, self.options.node_ipc_fd(), + self.options.serve_port(), + self.options.serve_host(), self.options.enable_future_features(), // TODO(bartlomieju): temporarily disabled // self.options.disable_deprecated_api_warning, diff --git a/cli/main.rs b/cli/main.rs index a4e93ca315..142ae017ce 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -33,6 +33,7 @@ use crate::util::display; use crate::util::v8::get_v8_flags_from_env; use crate::util::v8::init_v8_flags; +use deno_runtime::WorkerExecutionMode; pub use deno_runtime::UNSTABLE_GRANULAR_FLAGS; use deno_core::anyhow::Context; @@ -174,9 +175,12 @@ async fn run_subcommand(flags: Flags) -> Result { if run_flags.is_stdin() { tools::run::run_from_stdin(flags).await } else { - tools::run::run_script(flags, run_flags).await + tools::run::run_script(WorkerExecutionMode::Run, flags, run_flags.watch).await } }), + DenoSubcommand::Serve(serve_flags) => spawn_subcommand(async move { + tools::run::run_script(WorkerExecutionMode::Serve, flags, serve_flags.watch).await + }), DenoSubcommand::Task(task_flags) => spawn_subcommand(async { tools::task::execute_script(flags, task_flags).await }), diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 003e2bf793..8ff822f5a3 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -50,6 +50,7 @@ use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; +use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; use import_map::parse_from_json; @@ -567,6 +568,8 @@ pub async fn run( create_coverage_collector: None, }, None, + None, + None, false, // TODO(bartlomieju): temporarily disabled // metadata.disable_deprecated_api_warning, @@ -581,7 +584,11 @@ pub async fn run( deno_core::JsRuntime::init_platform(None); let mut worker = worker_factory - .create_main_worker(main_module.clone(), permissions) + .create_main_worker( + WorkerExecutionMode::Run, + main_module.clone(), + permissions, + ) .await?; let exit_code = worker.run().await?; diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs index 95986f3202..e2411ed125 100644 --- a/cli/tools/bench/mod.rs +++ b/cli/tools/bench/mod.rs @@ -35,6 +35,7 @@ use deno_core::PollEventLoopOptions; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use deno_runtime::tokio_util::create_and_run_current_thread; +use deno_runtime::WorkerExecutionMode; use indexmap::IndexMap; use indexmap::IndexSet; use log::Level; @@ -204,6 +205,7 @@ async fn bench_specifier_inner( ) -> Result<(), AnyError> { let mut worker = worker_factory .create_custom_worker( + WorkerExecutionMode::Bench, specifier.clone(), PermissionsContainer::new(permissions), vec![ops::bench::deno_bench::init_ops(sender.clone())], diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index 64c8cd7dfc..6531b0339a 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -22,6 +22,7 @@ use deno_runtime::deno_io::Stdio; use deno_runtime::deno_io::StdioPipe; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; +use deno_runtime::WorkerExecutionMode; use deno_terminal::colors; use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedSender; @@ -88,6 +89,7 @@ pub async fn kernel( let mut worker = worker_factory .create_custom_worker( + WorkerExecutionMode::Jupyter, main_module.clone(), permissions, vec![ diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index 03b8e512ee..d1c1cab713 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -15,6 +15,7 @@ use deno_core::serde_json; use deno_core::unsync::spawn_blocking; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; +use deno_runtime::WorkerExecutionMode; use rustyline::error::ReadlineError; mod channel; @@ -170,6 +171,7 @@ pub async fn run(flags: Flags, repl_flags: ReplFlags) -> Result { let test_event_sender = worker.sender; let mut worker = worker_factory .create_custom_worker( + WorkerExecutionMode::Repl, main_module.clone(), permissions, vec![crate::ops::testing::deno_test::init_ops(test_event_sender)], diff --git a/cli/tools/run/mod.rs b/cli/tools/run/mod.rs index 793b55a8ac..9f4bfeb964 100644 --- a/cli/tools/run/mod.rs +++ b/cli/tools/run/mod.rs @@ -5,10 +5,10 @@ use std::io::Read; use deno_core::error::AnyError; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; +use deno_runtime::WorkerExecutionMode; use crate::args::EvalFlags; use crate::args::Flags; -use crate::args::RunFlags; use crate::args::WatchFlagsWithPaths; use crate::factory::CliFactory; use crate::factory::CliFactoryBuilder; @@ -19,8 +19,9 @@ use crate::util::file_watcher::WatcherRestartMode; pub mod hmr; pub async fn run_script( + mode: WorkerExecutionMode, flags: Flags, - run_flags: RunFlags, + watch: Option, ) -> Result { if !flags.has_permission() && flags.has_permission_in_argv() { log::warn!( @@ -33,8 +34,8 @@ To grant permissions, set them before the script argument. For example: ); } - if let Some(watch_flags) = run_flags.watch { - return run_with_watch(flags, watch_flags).await; + if let Some(watch_flags) = watch { + return run_with_watch(mode, flags, watch_flags).await; } // TODO(bartlomieju): actually I think it will also fail if there's an import @@ -68,7 +69,7 @@ To grant permissions, set them before the script argument. For example: )?); let worker_factory = factory.create_cli_main_worker_factory().await?; let mut worker = worker_factory - .create_main_worker(main_module, permissions) + .create_main_worker(mode, main_module, permissions) .await?; let exit_code = worker.run().await?; @@ -98,7 +99,7 @@ pub async fn run_from_stdin(flags: Flags) -> Result { }); let mut worker = worker_factory - .create_main_worker(main_module, permissions) + .create_main_worker(WorkerExecutionMode::Run, main_module, permissions) .await?; let exit_code = worker.run().await?; Ok(exit_code) @@ -107,6 +108,7 @@ pub async fn run_from_stdin(flags: Flags) -> Result { // TODO(bartlomieju): this function is not handling `exit_code` set by the runtime // code properly. async fn run_with_watch( + mode: WorkerExecutionMode, flags: Flags, watch_flags: WatchFlagsWithPaths, ) -> Result { @@ -135,7 +137,7 @@ async fn run_with_watch( let mut worker = factory .create_cli_main_worker_factory() .await? - .create_main_worker(main_module, permissions) + .create_main_worker(mode, main_module, permissions) .await?; if watch_flags.hmr { @@ -184,7 +186,7 @@ pub async fn eval_command( )?); let worker_factory = factory.create_cli_main_worker_factory().await?; let mut worker = worker_factory - .create_main_worker(main_module, permissions) + .create_main_worker(WorkerExecutionMode::Eval, main_module, permissions) .await?; let exit_code = worker.run().await?; Ok(exit_code) diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 013d8c0848..ffa0fef9ee 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -59,6 +59,7 @@ use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use deno_runtime::tokio_util::create_and_run_current_thread; use deno_runtime::worker::MainWorker; +use deno_runtime::WorkerExecutionMode; use indexmap::IndexMap; use indexmap::IndexSet; use log::Level; @@ -583,6 +584,7 @@ async fn configure_main_worker( ) -> Result<(Option>, MainWorker), anyhow::Error> { let mut worker = worker_factory .create_custom_worker( + WorkerExecutionMode::Test, specifier.clone(), PermissionsContainer::new(permissions), vec![ops::testing::deno_test::init_ops(worker_sender.sender)], diff --git a/cli/worker.rs b/cli/worker.rs index 7dbb9b177a..edc4ef907a 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::num::NonZeroU16; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; @@ -40,6 +41,7 @@ use deno_runtime::web_worker::WebWorkerOptions; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use deno_runtime::BootstrapOptions; +use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReqReference; @@ -142,6 +144,8 @@ struct SharedWorkerState { disable_deprecated_api_warning: bool, verbose_deprecated_api_warning: bool, code_cache: Option>, + serve_port: Option, + serve_host: Option, } impl SharedWorkerState { @@ -410,6 +414,8 @@ impl CliMainWorkerFactory { feature_checker: Arc, options: CliMainWorkerOptions, node_ipc: Option, + serve_port: Option, + serve_host: Option, enable_future_features: bool, disable_deprecated_api_warning: bool, verbose_deprecated_api_warning: bool, @@ -434,6 +440,8 @@ impl CliMainWorkerFactory { maybe_lockfile, feature_checker, node_ipc, + serve_port, + serve_host, enable_future_features, disable_deprecated_api_warning, verbose_deprecated_api_warning, @@ -444,11 +452,13 @@ impl CliMainWorkerFactory { pub async fn create_main_worker( &self, + mode: WorkerExecutionMode, main_module: ModuleSpecifier, permissions: PermissionsContainer, ) -> Result { self .create_custom_worker( + mode, main_module, permissions, vec![], @@ -459,6 +469,7 @@ impl CliMainWorkerFactory { pub async fn create_custom_worker( &self, + mode: WorkerExecutionMode, main_module: ModuleSpecifier, permissions: PermissionsContainer, custom_extensions: Vec, @@ -545,7 +556,7 @@ impl CliMainWorkerFactory { let maybe_inspector_server = shared.maybe_inspector_server.clone(); let create_web_worker_cb = - create_web_worker_callback(shared.clone(), stdio.clone()); + create_web_worker_callback(mode, shared.clone(), stdio.clone()); let maybe_storage_key = shared .storage_key_resolver @@ -600,6 +611,9 @@ impl CliMainWorkerFactory { disable_deprecated_api_warning: shared.disable_deprecated_api_warning, verbose_deprecated_api_warning: shared.verbose_deprecated_api_warning, future: shared.enable_future_features, + mode, + serve_port: shared.serve_port, + serve_host: shared.serve_host.clone(), }, extensions: custom_extensions, startup_snapshot: crate::js::deno_isolate_init(), @@ -745,6 +759,7 @@ impl CliMainWorkerFactory { } fn create_web_worker_callback( + mode: WorkerExecutionMode, shared: Arc, stdio: deno_runtime::deno_io::Stdio, ) -> Arc { @@ -758,7 +773,7 @@ fn create_web_worker_callback( let maybe_source_map_getter = shared.module_loader_factory.create_source_map_getter(); let create_web_worker_cb = - create_web_worker_callback(shared.clone(), stdio.clone()); + create_web_worker_callback(mode, shared.clone(), stdio.clone()); let maybe_storage_key = shared .storage_key_resolver @@ -805,6 +820,9 @@ fn create_web_worker_callback( disable_deprecated_api_warning: shared.disable_deprecated_api_warning, verbose_deprecated_api_warning: shared.verbose_deprecated_api_warning, future: shared.enable_future_features, + mode, + serve_port: shared.serve_port, + serve_host: shared.serve_host.clone(), }, extensions: vec![], startup_snapshot: crate::js::deno_isolate_init(), diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 1063f9691a..d19b62ddec 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -791,8 +791,37 @@ internals.upgradeHttpRaw = upgradeHttpRaw; internals.serveHttpOnListener = serveHttpOnListener; internals.serveHttpOnConnection = serveHttpOnConnection; +function registerDeclarativeServer(exports) { + if (ObjectHasOwn(exports, "fetch")) { + if (typeof exports.fetch !== "function" || exports.fetch.length !== 1) { + throw new TypeError( + "Invalid type for fetch: must be a function with a single parameter", + ); + } + return ({ servePort, serveHost }) => { + Deno.serve({ + port: servePort, + hostname: serveHost, + onListen: ({ port, hostname }) => { + console.debug( + `%cdeno serve%c: Listening on %chttp://${hostname}:${port}/%c`, + "color: green", + "color: inherit", + "color: yellow", + "color: inherit", + ); + }, + handler: (req) => { + return exports.fetch(req); + }, + }); + }; + } +} + export { addTrailers, + registerDeclarativeServer, serve, serveHttpOnConnection, serveHttpOnListener, diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 8164e0c9a2..59d2a1434d 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -35,6 +35,7 @@ const { ObjectAssign, ObjectDefineProperties, ObjectDefineProperty, + ObjectHasOwn, ObjectKeys, ObjectPrototypeIsPrototypeOf, ObjectSetPrototypeOf, @@ -52,6 +53,7 @@ const { const { isNativeError, } = core; +import { registerDeclarativeServer } from "ext:deno_http/00_serve.ts"; import * as event from "ext:deno_web/02_event.js"; import * as location from "ext:deno_web/12_location.js"; import * as version from "ext:runtime/01_version.ts"; @@ -679,6 +681,18 @@ const { target, } = op_snapshot_options(); +const executionModes = { + none: 0, + worker: 1, + run: 2, + repl: 3, + eval: 4, + test: 5, + bench: 6, + serve: 7, + jupyter: 8, +}; + function bootstrapMainRuntime(runtimeOptions, warmup = false) { if (!warmup) { if (hasBootstrapped) { @@ -695,8 +709,52 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) { 7: shouldDisableDeprecatedApiWarning, 8: shouldUseVerboseDeprecatedApiWarning, 9: future, + 10: mode, + 11: servePort, + 12: serveHost, } = runtimeOptions; + if (mode === executionModes.run || mode === executionModes.serve) { + let serve = undefined; + core.addMainModuleHandler((main) => { + if (ObjectHasOwn(main, "default")) { + try { + serve = registerDeclarativeServer(main.default); + } catch (e) { + if (mode === executionModes.serve) { + throw e; + } + } + } + + if (mode === executionModes.serve && !serve) { + console.error( + `%cerror: %cdeno serve requires %cexport default { fetch }%c in the main module, did you mean to run \"deno run\"?`, + "color: yellow;", + "color: inherit;", + "font-weight: bold;", + "font-weight: normal;", + ); + return; + } + + if (serve) { + if (mode === executionModes.run) { + console.error( + `%cwarning: %cDetected %cexport default { fetch }%c, did you mean to run \"deno serve\"?`, + "color: yellow;", + "color: inherit;", + "font-weight: bold;", + "font-weight: normal;", + ); + } + if (mode === executionModes.serve) { + serve({ servePort, serveHost }); + } + } + }); + } + // TODO(iuioiua): remove in Deno v2. This allows us to dynamically delete // class properties within constructors for classes that are not defined // within the Deno namespace. diff --git a/runtime/lib.rs b/runtime/lib.rs index f33e9b7e33..ec751f207a 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -41,6 +41,7 @@ pub mod worker; mod worker_bootstrap; pub use worker_bootstrap::BootstrapOptions; +pub use worker_bootstrap::WorkerExecutionMode; pub use worker_bootstrap::WorkerLogLevel; mod shared; diff --git a/runtime/worker_bootstrap.rs b/runtime/worker_bootstrap.rs index c019dae1ac..ee3b81c628 100644 --- a/runtime/worker_bootstrap.rs +++ b/runtime/worker_bootstrap.rs @@ -4,10 +4,36 @@ use deno_core::v8; use deno_core::ModuleSpecifier; use serde::Serialize; use std::cell::RefCell; +use std::num::NonZeroU16; use std::thread; use deno_terminal::colors; +/// The execution mode for this worker. Some modes may have implicit behaviour. +#[derive(Copy, Clone)] +#[repr(u8)] +pub enum WorkerExecutionMode { + /// No special behaviour. + None, + + /// Running in a worker. + Worker, + /// `deno run` + Run, + /// `deno repl` + Repl, + /// `deno eval` + Eval, + /// `deno test` + Test, + /// `deno bench` + Bench, + /// `deno serve` + Serve, + /// `deno jupyter` + Jupyter, +} + /// The log level to use when printing diagnostic log messages, warnings, /// or errors in the worker. /// @@ -63,6 +89,10 @@ pub struct BootstrapOptions { pub disable_deprecated_api_warning: bool, pub verbose_deprecated_api_warning: bool, pub future: bool, + pub mode: WorkerExecutionMode, + // Used by `deno serve` + pub serve_port: Option, + pub serve_host: Option, } impl Default for BootstrapOptions { @@ -94,6 +124,9 @@ impl Default for BootstrapOptions { disable_deprecated_api_warning: false, verbose_deprecated_api_warning: false, future: false, + mode: WorkerExecutionMode::None, + serve_port: Default::default(), + serve_host: Default::default(), } } } @@ -129,6 +162,12 @@ struct BootstrapV8<'a>( bool, // future bool, + // mode + i32, + // serve port + u16, + // serve host + Option<&'a str>, ); impl BootstrapOptions { @@ -151,6 +190,9 @@ impl BootstrapOptions { self.disable_deprecated_api_warning, self.verbose_deprecated_api_warning, self.future, + self.mode as u8 as _, + self.serve_port.map(|x| x.into()).unwrap_or_default(), + self.serve_host.as_deref(), ); bootstrap.serialize(ser).unwrap() diff --git a/tests/specs/serve/bad/__test__.jsonc b/tests/specs/serve/bad/__test__.jsonc new file mode 100644 index 0000000000..9a37d60ffd --- /dev/null +++ b/tests/specs/serve/bad/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "serve --port 12345 main.ts", + "output": "main.out", + "tempDir": true +} diff --git a/tests/specs/serve/bad/main.out b/tests/specs/serve/bad/main.out new file mode 100644 index 0000000000..1bb42f909d --- /dev/null +++ b/tests/specs/serve/bad/main.out @@ -0,0 +1 @@ +error: deno serve requires export default { fetch } in the main module, did you mean to run "deno run"? diff --git a/tests/specs/serve/bad/main.ts b/tests/specs/serve/bad/main.ts new file mode 100644 index 0000000000..3399721d4c --- /dev/null +++ b/tests/specs/serve/bad/main.ts @@ -0,0 +1,4 @@ +export default { + bad() { + }, +}; diff --git a/tests/specs/serve/basic/__test__.jsonc b/tests/specs/serve/basic/__test__.jsonc new file mode 100644 index 0000000000..9a37d60ffd --- /dev/null +++ b/tests/specs/serve/basic/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "serve --port 12345 main.ts", + "output": "main.out", + "tempDir": true +} diff --git a/tests/specs/serve/basic/main.out b/tests/specs/serve/basic/main.out new file mode 100644 index 0000000000..e79d76af53 --- /dev/null +++ b/tests/specs/serve/basic/main.out @@ -0,0 +1 @@ +deno serve: Listening on http://localhost:12345/ diff --git a/tests/specs/serve/basic/main.ts b/tests/specs/serve/basic/main.ts new file mode 100644 index 0000000000..9e299ab219 --- /dev/null +++ b/tests/specs/serve/basic/main.ts @@ -0,0 +1,18 @@ +(async () => { + for (let i = 0; i < 1000; i++) { + try { + const resp = await fetch("http://localhost:12345/"); + Deno.exit(0); + } catch { + await new Promise((r) => setTimeout(r, 10)); + } + } + + Deno.exit(2); +})(); + +export default { + fetch(req) { + return new Response("Hello world!"); + }, +};