Rustで450行くらいで作るViteっぽいもの
はじめに
みなさんはもうこちらの素晴らしい記事は読まれましたでしょうか。
この記事を元にして、RustでViteっぽいものを実装しました(元記事はTSです)。
成果物
mvite dev
でHMR付きの開発サーバーが立ち上がります。mvite build
でバンドルします。
元記事のものと比較すると、Plugin機構がなかったりファイルが見つからない場合panicしたりするなどするのでタイトルは「っぽいもの」としました。至る所でunwrapしているのはそのうちどうにかします。
実装
サーバはActix Web、コンパイラはSWCを使っています。
TypeScriptコードのコンパイル
RustからSWCを使ってTypeScriptで書かれたコードをコンパイルするのには、以下を参照しました。
上記のコードはファイルパスをコマンドライン引数で受け取ってファイルを読み込み、JSに変換して標準出力に出すものです。
in/outを変えればそのまま動きます。
注意点として、swc_commonはfeatures = ["tty-emitter"]
もありでインストールしてください。
静的ファイルの配信
普通に静的ファイルを配信するだけなら、
にある通り HttpServer::new(|| App::new().route("/{filename:.*}", web::get().to(index)))
.bind(("127.0.0.1", 8080))?
.run()
.await
して
async fn index(req: HttpRequest) -> Result<NamedFile> {
let path: PathBuf = req.match_info().query("filename").parse().unwrap();
Ok(NamedFile::open(path)?)
}
とすれば良いです。しかし、Viteではファイルを変換する必要があります。また、ヘッダーフィールドも"application/javascript"に変更する必要があります。
そのため、
- 拡張子を見る(ない場合はJS/TSをつけて探します)
- TSの場合はJSにSWCで変換
- JSを以下のように返す
// pathは(変換した)JSファイルのパス
let mut res = NamedFile::open(path).unwrap().into_response(&req);
res.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/javascript"),
);
return res;
- JS/TSでない場合、 型を合わせるために
NamedFile::open(path).unwrap().into_response(&req)
を返す
という順序で評価します。
HMR
HMRは元記事にある通りファイルの変更検知+websocketサーバーで実現します。
ファイルの変更検知はnotifyを、websocketサーバーはActix Webのものを使いました。
struct MviteWs;
impl Actor for MviteWs {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
ctx.run_later(Duration::from_secs(5), |_, ctx| {
let (sender, receiver) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(
sender,
notify::Config::default().with_poll_interval(Duration::from_secs(2)),
)
.unwrap();
watcher
.watch(std::path::Path::new("."), RecursiveMode::Recursive)
.unwrap();
loop {
match receiver.recv() {
Ok(event) => {
ctx.text("{\"type\": \"reload\"}");
break;
}
Err(e) => println!("watch error: {:?}", e),
}
}
}
}
バンドル
バンドルは
まず、読み込み時にTSとして読み込むようにします。
let module = parse_file_as_module(
&fm,
//Syntax::Es(Default::default()),
Syntax::Typescript(TsConfig {
..Default::default()
}),
EsVersion::Es2020,
None,
&mut vec![],
)
次に出力時はJSとして出力するようにします。
let globals = Globals::default();
GLOBALS.set(&globals, || {
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();
let module = bundled.module;
let module =
module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true));
let module = module.fold_with(&mut strip(top_level_mark));
let module = module.fold_with(&mut hygiene());
emitter.emit_module(&module).unwrap();
});
おわりに
正直まだ成果物のコードのクオリティは低いですが、動くようにするのも大変だったので、一旦出しました。
Discussion