feat: deno init --serve (#24897)

This commit adds "--serve" flag to "deno init" subcommand,
that provides a template for quick starting a project using
"deno serve".

---------

Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
Bartek Iwańczuk 2024-08-08 17:54:39 +01:00 committed by GitHub
parent 3f692bed0a
commit ffe1bfd54c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 303 additions and 7 deletions

View File

@ -214,6 +214,7 @@ impl FmtFlags {
pub struct InitFlags {
pub dir: Option<String>,
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::<String>("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()
}

View File

@ -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!("");

View File

@ -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();
}

View File

@ -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<Response>;
/**
* 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<Response>,
): (
request: Request,
info?: Deno.ServeHandlerInfo,
) => Response | Promise<Response> {
// 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<Response> {
return new Response("hello world")
}

View File

@ -0,0 +1,5 @@
{
"exports": {
".": "./mod.ts"
}
}

View File

@ -0,0 +1,8 @@
{
"scope": "std",
"name": "http",
"latest": "1.0.0",
"versions": {
"1.0.0": {}
}
}