diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 1053e8f3f1..e28ce549b4 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -214,6 +214,7 @@ impl FmtFlags { pub struct InitFlags { pub dir: Option, pub lib: bool, + pub serve: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -2121,6 +2122,14 @@ fn init_subcommand() -> Command { .required(false) .action(ArgAction::SetTrue), ) + .arg( + Arg::new("serve") + .long("serve") + .long_help("Generate an example project for `deno serve`") + .conflicts_with("lib") + .required(false) + .action(ArgAction::SetTrue), + ) }) } @@ -4174,6 +4183,7 @@ fn init_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.subcommand = DenoSubcommand::Init(InitFlags { dir: matches.remove_one::("dir"), lib: matches.get_flag("lib"), + serve: matches.get_flag("serve"), }); } @@ -10026,7 +10036,8 @@ mod tests { Flags { subcommand: DenoSubcommand::Init(InitFlags { dir: None, - lib: false + lib: false, + serve: false, }), ..Flags::default() } @@ -10038,7 +10049,8 @@ mod tests { Flags { subcommand: DenoSubcommand::Init(InitFlags { dir: Some(String::from("foo")), - lib: false + lib: false, + serve: false, }), ..Flags::default() } @@ -10050,7 +10062,8 @@ mod tests { Flags { subcommand: DenoSubcommand::Init(InitFlags { dir: None, - lib: false + lib: false, + serve: false, }), log_level: Some(Level::Error), ..Flags::default() @@ -10063,7 +10076,21 @@ mod tests { Flags { subcommand: DenoSubcommand::Init(InitFlags { dir: None, - lib: true + lib: true, + serve: false, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "init", "--serve"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Init(InitFlags { + dir: None, + lib: false, + serve: true, }), ..Flags::default() } @@ -10075,7 +10102,8 @@ mod tests { Flags { subcommand: DenoSubcommand::Init(InitFlags { dir: Some(String::from("foo")), - lib: true + lib: true, + serve: false, }), ..Flags::default() } diff --git a/cli/tools/init/mod.rs b/cli/tools/init/mod.rs index c62d932892..6d442198ee 100644 --- a/cli/tools/init/mod.rs +++ b/cli/tools/init/mod.rs @@ -20,7 +20,87 @@ pub fn init_project(init_flags: InitFlags) -> Result<(), AnyError> { cwd }; - if init_flags.lib { + if init_flags.serve { + create_file( + &dir, + "main.ts", + r#"import { type Route, route, serveDir } from "@std/http"; + +const routes: Route[] = [ + { + pattern: new URLPattern({ pathname: "/" }), + handler: () => new Response("Home page"), + }, + { + pattern: new URLPattern({ pathname: "/users/:id" }), + handler: (_req, _info, params) => new Response(params?.pathname.groups.id), + }, + { + pattern: new URLPattern({ pathname: "/static/*" }), + handler: (req) => serveDir(req, { urlRoot: "./" }), + }, +]; + +function defaultHandler(_req: Request) { + return new Response("Not found", { status: 404 }); +} + +const handler = route(routes, defaultHandler); + +export default { + fetch(req) { + return handler(req); + }, +} satisfies Deno.ServeDefaultExport; + +"#, + )?; + create_file( + &dir, + "main_test.ts", + r#"import { assertEquals } from "@std/assert"; +import server from "./main.ts"; + +Deno.test(async function serverFetch() { + const req = new Request("https://deno.land"); + const res = await server.fetch(req); + assertEquals(await res.text(), "Home page"); +}); + +Deno.test(async function serverFetchNotFound() { + const req = new Request("https://deno.land/404"); + const res = await server.fetch(req); + assertEquals(res.status, 404); +}); + +Deno.test(async function serverFetchUsers() { + const req = new Request("https://deno.land/users/123"); + const res = await server.fetch(req); + assertEquals(await res.text(), "123"); +}); + +Deno.test(async function serverFetchStatic() { + const req = new Request("https://deno.land/static/main.ts"); + const res = await server.fetch(req); + assertEquals(res.headers.get("content-type"), "text/plain;charset=UTF-8"); +}); +"#, + )?; + + create_json_file( + &dir, + "deno.json", + &json!({ + "tasks": { + "dev": "deno serve --watch -R main.ts", + }, + "imports": { + "@std/assert": "jsr:@std/assert@1", + "@std/http": "jsr:@std/http@1", + } + }), + )?; + } else if init_flags.lib { // Extract the directory name to use as the project name let project_name = dir .file_name() @@ -111,7 +191,19 @@ Deno.test(function addTest() { info!(" cd {}", dir); info!(""); } - if init_flags.lib { + if init_flags.serve { + info!(" {}", colors::gray("# Run the server")); + info!(" deno serve -R main.ts"); + info!(""); + info!( + " {}", + colors::gray("# Run the server and watch for file changes") + ); + info!(" deno task dev"); + info!(""); + info!(" {}", colors::gray("# Run the tests")); + info!(" deno -R test"); + } else if init_flags.lib { info!(" {}", colors::gray("# Run the tests")); info!(" deno test"); info!(""); diff --git a/tests/integration/init_tests.rs b/tests/integration/init_tests.rs index 65a57eeea4..757dcb021e 100644 --- a/tests/integration/init_tests.rs +++ b/tests/integration/init_tests.rs @@ -170,3 +170,50 @@ Run these commands to get started output.assert_exit_code(0); output.assert_matches_text("Log from main.ts that already exists\n"); } + +#[tokio::test] +async fn init_subcommand_serve() { + let context = TestContextBuilder::for_jsr().use_temp_cwd().build(); + let cwd = context.temp_dir().path(); + + let output = context + .new_command() + .args("init --serve") + .split_output() + .run(); + + output.assert_exit_code(0); + + let stderr = output.stderr(); + assert_contains!(stderr, "Project initialized"); + assert_contains!(stderr, "deno serve -R main.ts"); + assert_contains!(stderr, "deno task dev"); + assert_contains!(stderr, "deno -R test"); + + assert!(cwd.join("deno.json").exists()); + + let mut child = context + .new_command() + .env("NO_COLOR", "1") + .args("serve -R --port 9500 main.ts") + .spawn_with_piped_output(); + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + let resp = reqwest::get("http://127.0.0.1:9500").await.unwrap(); + + let body = resp.text().await.unwrap(); + assert_eq!(body, "Home page"); + + let _ = child.kill(); + + let output = context + .new_command() + .env("NO_COLOR", "1") + .args("-R test") + .split_output() + .run(); + + output.assert_exit_code(0); + assert_contains!(output.stdout(), "4 passed"); + output.skip_output_check(); +} diff --git a/tests/registry/jsr/@std/http/1.0.0/mod.ts b/tests/registry/jsr/@std/http/1.0.0/mod.ts new file mode 100644 index 0000000000..0a0e82847f --- /dev/null +++ b/tests/registry/jsr/@std/http/1.0.0/mod.ts @@ -0,0 +1,116 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/** + * Request handler for {@linkcode Route}. + * + * > [!WARNING] + * > **UNSTABLE**: New API, yet to be vetted. + * + * @experimental + * + * Extends {@linkcode Deno.ServeHandlerInfo} by adding adding a `params` argument. + * + * @param request Request + * @param info Request info + * @param params URL pattern result + */ +export type Handler = ( + request: Request, + info?: Deno.ServeHandlerInfo, + params?: URLPatternResult | null, +) => Response | Promise; + +/** + * Route configuration for {@linkcode route}. + * + * > [!WARNING] + * > **UNSTABLE**: New API, yet to be vetted. + * + * @experimental + */ +export interface Route { + /** + * Request URL pattern. + */ + pattern: URLPattern; + /** + * Request method. + * + * @default {"GET"} + */ + method?: string; + /** + * Request handler. + */ + handler: Handler; +} + +/** + * Routes requests to different handlers based on the request path and method. + * + * > [!WARNING] + * > **UNSTABLE**: New API, yet to be vetted. + * + * @experimental + * + * @example Usage + * ```ts no-eval + * import { route, type Route } from "@std/http/route"; + * import { serveDir } from "@std/http/file-server"; + * + * const routes: Route[] = [ + * { + * pattern: new URLPattern({ pathname: "/about" }), + * handler: () => new Response("About page"), + * }, + * { + * pattern: new URLPattern({ pathname: "/users/:id" }), + * handler: (_req, _info, params) => new Response(params?.pathname.groups.id), + * }, + * { + * pattern: new URLPattern({ pathname: "/static/*" }), + * handler: (req: Request) => serveDir(req) + * } + * ]; + * + * function defaultHandler(_req: Request) { + * return new Response("Not found", { status: 404 }); + * } + * + * Deno.serve(route(routes, defaultHandler)); + * ``` + * + * @param routes Route configurations + * @param defaultHandler Default request handler that's returned when no route + * matches the given request. Serving HTTP 404 Not Found or 405 Method Not + * Allowed response can be done in this function. + * @returns Request handler + */ +export function route( + routes: Route[], + defaultHandler: ( + request: Request, + info?: Deno.ServeHandlerInfo, + ) => Response | Promise, +): ( + request: Request, + info?: Deno.ServeHandlerInfo, +) => Response | Promise { + // TODO(iuioiua): Use `URLPatternList` once available (https://github.com/whatwg/urlpattern/pull/166) + return (request: Request, info?: Deno.ServeHandlerInfo) => { + for (const route of routes) { + const match = route.pattern.exec(request.url); + if (match) return route.handler(request, info, match); + } + return defaultHandler(request, info); + }; +} + +interface ServeDirOptions { + urlRoot?: string; +} + +// NOTE(bartlomieju): not important, just for testing +export async function serveDir(req: Request, opts: ServeDirOptions = {}): Response | Promise { + return new Response("hello world") +} diff --git a/tests/registry/jsr/@std/http/1.0.0_meta.json b/tests/registry/jsr/@std/http/1.0.0_meta.json new file mode 100644 index 0000000000..9b5dd9d0a5 --- /dev/null +++ b/tests/registry/jsr/@std/http/1.0.0_meta.json @@ -0,0 +1,5 @@ +{ + "exports": { + ".": "./mod.ts" + } +} \ No newline at end of file diff --git a/tests/registry/jsr/@std/http/meta.json b/tests/registry/jsr/@std/http/meta.json new file mode 100644 index 0000000000..a103326386 --- /dev/null +++ b/tests/registry/jsr/@std/http/meta.json @@ -0,0 +1,8 @@ +{ + "scope": "std", + "name": "http", + "latest": "1.0.0", + "versions": { + "1.0.0": {} + } +}