Skip to content

Commit f38aff8

Browse files
committed
support remote module loading
1 parent c0e1432 commit f38aff8

File tree

6 files changed

+126
-20
lines changed

6 files changed

+126
-20
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ deno_web = "0.245.0"
2828
deno_webidl = "0.214.0"
2929
http = "1.3.1"
3030
rand = "0.9.2"
31+
reqwest = "0.12.23"
3132
rustls = "0.23.28"
3233
serde = { version = "1.0", features = ["derive"] }
3334
serde_json = "1.0"

crates/funcgg-runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ deno_url = { workspace = true }
2323
deno_web = { workspace = true }
2424
deno_webidl = { workspace = true }
2525
http = { workspace = true }
26+
reqwest = { workspace = true }
2627
rustls = { workspace = true }
2728
serde = { workspace = true }
2829
serde_json = { workspace = true }

crates/funcgg-runtime/src/js/worker.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
async function resolveHandler() {
22
const mod = await import("func:user-code");
33

4-
if (typeof mod.handler === "function") {
5-
return mod.handler;
6-
}
4+
const methods = ["fetch", "handler"];
5+
6+
for (const method of methods) {
7+
if (typeof mod[method] === "function") {
8+
return mod[method];
9+
}
710

8-
if (
9-
typeof mod.default === "object" &&
10-
typeof mod.default.handler === "function"
11-
) {
12-
return mod.default.handler;
11+
if (
12+
typeof mod.default === "object" &&
13+
typeof mod.default[method] === "function"
14+
) {
15+
return mod.default[method];
16+
}
1317
}
1418

1519
if (typeof mod.default === "function") {

crates/funcgg-runtime/src/loader.rs

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1+
use std::borrow::Cow;
2+
use std::time::Duration;
3+
14
use deno_ast::{MediaType, ParseParams};
25
use deno_core::{
3-
ModuleCodeString, ModuleLoadResponse, ModuleName, ModuleSpecifier, SourceMapData,
4-
error::ModuleLoaderError,
6+
ModuleCodeString, ModuleLoadResponse, ModuleName, ModuleSource, ModuleSourceCode,
7+
ModuleSpecifier, ModuleType, SourceMapData, error::ModuleLoaderError, futures::FutureExt,
58
};
69
use deno_error::JsErrorBox;
710

8-
pub struct ModuleLoader;
11+
pub struct ModuleLoader {
12+
http_client: reqwest::Client,
13+
}
914

1015
impl ModuleLoader {
1116
pub fn new() -> Self {
12-
Self {}
17+
let http_client = reqwest::Client::builder()
18+
.connect_timeout(Duration::from_secs(10))
19+
.user_agent("func.gg/module_loader")
20+
.build()
21+
.expect("Unable to build HTTP client");
22+
23+
Self { http_client }
1324
}
1425
}
1526

@@ -26,15 +37,87 @@ impl deno_core::ModuleLoader for ModuleLoader {
2637

2738
fn load(
2839
&self,
29-
module_specifier: &ModuleSpecifier,
40+
specifier: &ModuleSpecifier,
3041
_maybe_referrer: Option<&ModuleSpecifier>,
31-
_is_dynamic: bool,
42+
is_dyn_import: bool,
3243
_requested_module_type: deno_core::RequestedModuleType,
33-
) -> deno_core::ModuleLoadResponse {
34-
tracing::error!("attempting to load module: {}", module_specifier);
35-
ModuleLoadResponse::Sync(Err(ModuleLoaderError::generic(
36-
"module loading is not supported",
37-
)))
44+
) -> ModuleLoadResponse {
45+
if is_dyn_import {
46+
return ModuleLoadResponse::Sync(Err(ModuleLoaderError::generic(
47+
"dynamic module loading is not supported",
48+
)));
49+
}
50+
51+
if specifier.scheme() != "https" {
52+
return ModuleLoadResponse::Sync(Err(ModuleLoaderError::generic(
53+
"only modules with an 'https' scheme are supported",
54+
)));
55+
}
56+
57+
let specifier = specifier.clone();
58+
let http_client = self.http_client.clone();
59+
return ModuleLoadResponse::Async(
60+
async move {
61+
let res = http_client
62+
.get(specifier.clone())
63+
.query(&[("target", "deno")]) // tells services to explicitly redirect
64+
.send()
65+
.await
66+
.map_err(|err| ModuleLoaderError::generic(err.to_string()))?;
67+
68+
let original_specifier = {
69+
let mut url = specifier.clone();
70+
url.set_query(None);
71+
url
72+
};
73+
74+
let found_specifier = {
75+
let mut url = res.url().clone();
76+
url.set_query(None);
77+
url
78+
};
79+
80+
let content_type = res
81+
.headers()
82+
.get("content-type")
83+
.map(|ct| ct.to_str().unwrap_or_default())
84+
.unwrap_or_default()
85+
.split(';')
86+
.next()
87+
.unwrap_or_default()
88+
.trim()
89+
.to_lowercase();
90+
let module_type = match content_type.as_ref() {
91+
"application/javascript"
92+
| "text/javascript"
93+
| "application/ecmascript"
94+
| "text/ecmascript" => ModuleType::JavaScript,
95+
"application/wasm" => ModuleType::Wasm,
96+
"application/json" | "text/json" => ModuleType::Json,
97+
"text/plain" | "application/octet-stream" => ModuleType::Text,
98+
s => ModuleType::Other(Cow::Owned(s.to_string())),
99+
};
100+
101+
if !res.status().is_success() {
102+
return Err(ModuleLoaderError::generic("failed to load module"));
103+
}
104+
105+
// TODO: probably use bytes
106+
let src_text = res
107+
.text()
108+
.await
109+
.map_err(|err| ModuleLoaderError::generic(err.to_string()))?;
110+
111+
return Ok(ModuleSource::new_with_redirect(
112+
module_type,
113+
ModuleSourceCode::String(src_text.into()),
114+
&original_specifier,
115+
&found_specifier,
116+
None,
117+
));
118+
}
119+
.boxed_local(),
120+
);
38121
}
39122
}
40123

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Hono } from "https://esm.sh/hono";
2+
3+
const app = new Hono();
4+
5+
app.get("/", (c) => c.text("Hello World"));
6+
7+
export default app;

crates/funcgg-worker/src/routes.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ use crate::pool::Pool;
1717
pub async fn invoke(State(pool): State<Arc<Pool>>, request: Request) -> Response {
1818
// TODO: temporary, eventually this should be served dynamically
1919
// possibly serve https://github.com/denoland/eszip
20-
let js_code = include_str!("../examples/basic.js");
20+
let js_code = match tokio::fs::read_to_string("crates/funcgg-worker/examples/basic.js").await {
21+
Ok(code) => code,
22+
Err(err) => {
23+
tracing::error!("Failed to load JavaScript code: {}", err);
24+
return (
25+
StatusCode::INTERNAL_SERVER_ERROR,
26+
"Failed to load handler code",
27+
)
28+
.into_response();
29+
}
30+
};
2131

2232
let method = request.method().to_string();
2333
let path_and_query = request.uri().path_and_query().unwrap().to_owned();

0 commit comments

Comments
 (0)