Skip to content

Commit 4f106c8

Browse files
committed
prog
1 parent 911ca96 commit 4f106c8

File tree

19 files changed

+264
-61
lines changed

19 files changed

+264
-61
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ rand = { version = "0.8" }
5959
redb = { version = "2" }
6060
reqwest = { version = "0.12", default-features = false }
6161
#rust-embed = { version = "8.4" }
62-
#serde = { version = "1", features = ["derive"] }
63-
#serde_json = { version = "1" }
62+
serde = { version = "1", features = ["derive"] }
63+
serde_json = { version = "1" }
6464
#sieve-cache = { version = "0.2" }
6565
#strum = { version = "0.26" }
6666
syn = { version = "2" }

rust/ct-builder/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ deno_emit = { workspace = true }
1919
deno_graph = { workspace = true }
2020
redb = { workspace = true }
2121
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "charset", "http2", "macos-system-configuration"] }
22+
serde = { workspace = true }
23+
serde_json = { workspace = true }
2224
tempfile = { workspace = true }
2325
thiserror = { workspace = true }
2426
tokio = { workspace = true, features = ["rt-multi-thread", "io-util", "process", "fs"] }

rust/ct-builder/src/bin/builder.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ struct Cli {
1717
/// HTTP server port.
1818
#[arg(short = 'p', long, default_value_t = 8081)]
1919
pub http_port: u16,
20+
/// HTTP server port.
21+
#[arg(short = 'i', long)]
22+
pub import_map: Option<std::path::PathBuf>,
2023
}
2124

2225
#[cfg(not(target_arch = "wasm32"))]
2326
#[tokio::main]
2427
pub async fn main() -> Result<(), ct_builder::Error> {
2528
use clap::Parser;
26-
use ct_builder::{serve, BuildServerConfig};
29+
use ct_builder::{serve, BuildServerConfig, ImportMap};
2730
use std::net::SocketAddr;
31+
use std::path::PathBuf;
2832
use tracing_subscriber::{EnvFilter, FmtSubscriber};
2933

3034
let subscriber = FmtSubscriber::builder()
@@ -44,9 +48,20 @@ pub async fn main() -> Result<(), ct_builder::Error> {
4448
info!("HTTP listening on {}", socket_address);
4549
tokio::net::TcpListener::bind(socket_address).await?
4650
};
47-
let config = BuildServerConfig::default()
51+
let mut config = BuildServerConfig::default()
4852
.with_grpc(grpc_listener)
4953
.with_http(http_listener);
54+
if let Some(import_map_path) = cli.import_map {
55+
let import_map_path = if import_map_path.is_relative() {
56+
std::env::current_dir()?
57+
.join(import_map_path)
58+
.canonicalize()?
59+
} else {
60+
import_map_path
61+
};
62+
let import_map = ImportMap::from_path(import_map_path).await?;
63+
config = config.with_import_map(import_map);
64+
}
5065
serve(config).await?;
5166

5267
Ok(())

rust/ct-builder/src/builder.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
artifact::Artifact,
33
error::Error,
44
storage::{JsComponentStorage, PersistedHashStorage},
5-
JavaScriptBundler,
5+
ImportMap, JavaScriptBundler,
66
};
77
use async_trait::async_trait;
88
use blake3::Hash;
@@ -17,6 +17,7 @@ use tonic::{Request, Response, Status};
1717
pub struct BuildComponentConfig {
1818
pub definition: ModuleDefinition,
1919
pub bundle_common_imports: bool,
20+
pub import_map: Option<ImportMap>,
2021
}
2122

2223
#[derive(Clone)]
@@ -33,7 +34,11 @@ impl Builder {
3334
info!("Building source: {:#?}", config.definition.source);
3435
let artifact = match config.definition.content_type {
3536
ContentType::JavaScript => {
36-
JavaScriptBundler::bundle_from_bytes_sync(config.definition.source.into()).await?
37+
JavaScriptBundler::bundle_from_bytes_sync(
38+
config.definition.source.into(),
39+
config.import_map,
40+
)
41+
.await?
3742
}
3843
};
3944
self.storage.write(artifact).await
@@ -61,6 +66,7 @@ impl BuilderProto for Builder {
6166
.build(BuildComponentConfig {
6267
definition,
6368
bundle_common_imports: false,
69+
import_map: None,
6470
})
6571
.await?;
6672

rust/ct-builder/src/bundle.rs

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Utilities for compiling/bundling JavaScript into
22
//! a single source.
33
4+
use crate::{artifact::Artifact, Error, ImportMap};
45
use anyhow::anyhow;
56
use deno_emit::{
67
bundle, BundleOptions, BundleType, EmitOptions, LoadFuture, LoadOptions, Loader,
@@ -10,8 +11,6 @@ use deno_graph::source::LoadResponse;
1011
use reqwest::Client;
1112
use url::Url;
1213

13-
use crate::{artifact::Artifact, Error};
14-
1514
// Root module must have `.tsx` in order to be
1615
// interprete as Typescript/JSX.
1716
const ROOT_MODULE_URL: &str = "bundler:root.tsx";
@@ -20,13 +19,15 @@ const ROOT_MODULE_SCHEME: &str = "bundler";
2019
struct JavaScriptLoader {
2120
root: Option<Vec<u8>>,
2221
client: Client,
22+
import_map: Option<ImportMap>,
2323
}
2424

2525
impl JavaScriptLoader {
26-
pub fn new(root: Option<Vec<u8>>) -> Self {
26+
pub fn new(root: Option<Vec<u8>>, import_map: Option<ImportMap>) -> Self {
2727
Self {
2828
root,
2929
client: Client::new(),
30+
import_map,
3031
}
3132
}
3233
}
@@ -35,8 +36,8 @@ impl Loader for JavaScriptLoader {
3536
fn load(&self, specifier: &ModuleSpecifier, _options: LoadOptions) -> LoadFuture {
3637
let root = self.root.clone();
3738
let client = self.client.clone();
39+
let import_map = self.import_map.clone();
3840
let specifier = specifier.clone();
39-
4041
debug!("Attempting to load '{}'", specifier);
4142

4243
Box::pin(async move {
@@ -62,31 +63,55 @@ impl Loader for JavaScriptLoader {
6263
let bytes = response.bytes().await?;
6364
let content = bytes.to_vec().into();
6465

66+
let maybe_headers = Some(
67+
headers
68+
.into_iter()
69+
.filter_map(|(h, v)| {
70+
h.map(|header| {
71+
(
72+
header.to_string(),
73+
v.to_str().unwrap_or_default().to_string(),
74+
)
75+
})
76+
})
77+
.collect(),
78+
);
79+
trace!("maybe_headers {:#?}", maybe_headers);
6580
trace!("Loaded remote module: {}", String::from_utf8_lossy(&bytes));
6681
Ok(Some(LoadResponse::Module {
6782
content,
6883
specifier,
69-
maybe_headers: Some(
70-
headers
71-
.into_iter()
72-
.filter_map(|(h, v)| {
73-
h.map(|header| {
74-
(
75-
header.to_string(),
76-
v.to_str().unwrap_or_default().to_string(),
77-
)
78-
})
79-
})
80-
.collect(),
81-
),
84+
maybe_headers,
8285
}))
8386
}
8487
"node" | "npm" => Err(anyhow!(
8588
"Could not import '{specifier}'. Node.js and NPM modules are not supported."
8689
)),
87-
_ => Err(anyhow!(
88-
"Could not import '{specifier}'. Unrecognize specifier format.'"
89-
)),
90+
_ => {
91+
let specifier_str = specifier.to_string();
92+
debug!("Attempting to load {} from import map", specifier_str);
93+
if let Some(import_map) = import_map {
94+
if let Some(module_path) = import_map.get(&specifier_str) {
95+
let module_str = tokio::fs::read_to_string(module_path).await?;
96+
97+
// `LoadResponse::Module` appears to require at least
98+
// *some* headers.
99+
let headers = Some(std::collections::HashMap::from([(
100+
"content-type".into(),
101+
"text/javascript".into(),
102+
)]));
103+
104+
return Ok(Some(LoadResponse::Module {
105+
content: module_str.into_bytes().into(),
106+
specifier,
107+
maybe_headers: headers,
108+
}));
109+
}
110+
}
111+
Err(anyhow!(
112+
"Could not import '{specifier}'. Unrecognize specifier format.'"
113+
))
114+
}
90115
}
91116
})
92117
}
@@ -114,15 +139,21 @@ impl JavaScriptBundler {
114139
}
115140

116141
/// Bundle a JavaScript module via URL.
117-
pub async fn bundle_from_url(url: Url) -> Result<Artifact, Error> {
118-
let mut loader = JavaScriptLoader::new(None);
142+
pub async fn bundle_from_url(
143+
url: Url,
144+
import_map: Option<ImportMap>,
145+
) -> Result<Artifact, Error> {
146+
let mut loader = JavaScriptLoader::new(None, import_map);
119147
let emit = bundle(url, &mut loader, None, Self::bundle_options()).await?;
120148
Ok(emit.into())
121149
}
122150

123151
/// Bundle a JavaScript module from bytes.
124-
pub async fn bundle_from_bytes(module: Vec<u8>) -> Result<Artifact, Error> {
125-
let mut loader = JavaScriptLoader::new(Some(module));
152+
pub async fn bundle_from_bytes(
153+
module: Vec<u8>,
154+
import_map: Option<ImportMap>,
155+
) -> Result<Artifact, Error> {
156+
let mut loader = JavaScriptLoader::new(Some(module), import_map);
126157
let emit = bundle(
127158
Url::parse(ROOT_MODULE_URL).map_err(|error| Error::Internal(format!("{error}")))?,
128159
&mut loader,
@@ -136,18 +167,25 @@ impl JavaScriptBundler {
136167
/// Spawns a blocking bundle operation on a thread dedicated to blocking
137168
/// operations. This is needed in cases where bundling is taking place e.g.,
138169
/// within a web server.
139-
pub async fn bundle_from_bytes_sync(source_code: Vec<u8>) -> Result<Artifact, Error> {
170+
pub async fn bundle_from_bytes_sync(
171+
source_code: Vec<u8>,
172+
import_map: Option<ImportMap>,
173+
) -> Result<Artifact, Error> {
140174
tokio::task::spawn_blocking(move || {
141-
tokio::runtime::Handle::current()
142-
.block_on(JavaScriptBundler::bundle_from_bytes(source_code))
175+
tokio::runtime::Handle::current().block_on(JavaScriptBundler::bundle_from_bytes(
176+
source_code,
177+
import_map,
178+
))
143179
})
144180
.await?
145181
}
146182
}
147183

148184
#[cfg(test)]
149185
pub mod tests {
150-
use crate::JavaScriptBundler;
186+
use std::path::PathBuf;
187+
188+
use crate::{ImportMap, JavaScriptBundler};
151189
use anyhow::Result;
152190
use ct_test_fixtures::EsmTestServer;
153191
use ct_tracing::ct_tracing;
@@ -165,7 +203,7 @@ pub mod tests {
165203
let mut server = EsmTestServer::default();
166204
let addr = server.start().await?;
167205
let candidate = Url::parse(&format!("http://{}/math/index.js", addr))?;
168-
let bundle = JavaScriptBundler::bundle_from_url(candidate).await?;
206+
let bundle = JavaScriptBundler::bundle_from_url(candidate, None).await?;
169207

170208
assert_math_bundle(&bundle.component);
171209

@@ -178,7 +216,7 @@ pub mod tests {
178216
let mut server = EsmTestServer::default();
179217
let addr = server.start().await?;
180218
let candidate = Url::parse(&format!("http://{}/math/index.ts", addr))?;
181-
let bundle = JavaScriptBundler::bundle_from_url(candidate).await?;
219+
let bundle = JavaScriptBundler::bundle_from_url(candidate, None).await?;
182220

183221
assert_math_bundle(&bundle.component);
184222

@@ -195,7 +233,7 @@ pub mod tests {
195233
"#,
196234
addr
197235
);
198-
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into()).await?;
236+
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into(), None).await?;
199237

200238
assert_math_bundle(&bundle.component);
201239

@@ -212,7 +250,7 @@ export const add = function add(x: number, y: number): number {
212250
"#
213251
.to_string();
214252

215-
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into()).await?;
253+
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into(), None).await?;
216254
assert!(bundle.component.contains("function add"));
217255

218256
Ok(())
@@ -230,12 +268,30 @@ console.log(read, write);
230268
"#
231269
.to_string();
232270

233-
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into()).await?;
271+
let bundle = JavaScriptBundler::bundle_from_bytes(candidate.into(), None).await?;
234272

235273
assert!(bundle
236274
.component
237275
.contains("import { read, write } from \"common:io/state@0.0.1\""));
238276

239277
Ok(())
240278
}
279+
280+
#[tokio::test]
281+
#[ct_tracing]
282+
async fn it_bundles_modules_from_import_map() -> Result<()> {
283+
let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
284+
let import_map_path = fixtures_dir.join("imports.json");
285+
let import_map = ImportMap::from_path(import_map_path).await?;
286+
let candidate = r#"
287+
import { add } from "test:math";
288+
console.log(add(3, 5));
289+
"#
290+
.to_string();
291+
292+
let bundle =
293+
JavaScriptBundler::bundle_from_bytes(candidate.into(), Some(import_map)).await?;
294+
assert!(bundle.component.contains("a + b"));
295+
Ok(())
296+
}
241297
}

rust/ct-builder/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use redb::{CommitError, DatabaseError, StorageError, TableError, TransactionErro
33
use tokio::task::JoinError;
44
use tracing::subscriber::SetGlobalDefaultError;
55

6+
/// [`std::result::Result`] type with [Error] /// as its error.
7+
pub type Result<T> = ::std::result::Result<T, Error>;
8+
69
/// Errors from various builder operations.
710
#[derive(Debug, thiserror::Error)]
811
pub enum Error {
@@ -109,3 +112,9 @@ impl From<axum::http::StatusCode> for Error {
109112
Error::Internal(value.to_string())
110113
}
111114
}
115+
116+
impl From<serde_json::Error> for Error {
117+
fn from(value: serde_json::Error) -> Self {
118+
Error::Internal(value.to_string())
119+
}
120+
}

0 commit comments

Comments
 (0)