wip: move out of dioxus main

This commit is contained in:
Jonathan Kelley committed 2021-07-07 16:54:14 -04:00
commit aebf6678a8
14 files changed
+696

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
}
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "dioxus-studio"
version = "0.1.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
description = "CLI tool for developing, testing, and publishing Dioxus apps"
"license" = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
thiserror = "1.0.23"
log = "0.4.13"
fern = { version="0.6.0", features=["colored"] }
wasm-bindgen-cli-support = "0.2.73"
anyhow = "1.0.38"
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
async-std = { version="1.9.0", features=["attributes"] }
tide = "0.15.0"
fs_extra = "1.2.0"
cargo_toml = "0.8.1"
futures = "0.3.12"
notify = "5.0.0-pre.4"
rjdebounce = "0.2.1"
tempfile = "3.2.0"
[[bin]]
path = "src/main.rs"
name = "dioxus"
+48
View File
@@ -0,0 +1,48 @@
<div align="center">
<h1>📦✨ dioxus-cli</h1>
<p>
<strong>Tooling to supercharge dioxus projects</strong>
</p>
</div>
# About
---
dioxus-cli (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development just a simple two commands: `cargo init` and `dioxus-cli publish`.
Best thing:
- No NPM.
- No Webpack.
- No `node_modules`.
- No Babel
- No parcel
- No rollup
- No ESLint
Just install Rust, dioxus-cli, and you're good to go.
`cargo install --git github.com/jkelleyrtp/dioxus-cli`
Need a development server?
`dioxus develop`
Need to run an example?
`dioxus develop --example textbox`
Need to benchmark a component?
`dioxus bench`
Need to test your code?
`dioxus test`
Need to build your code into a bundle?
`dioxus build --outdir public`
Need to publish your code to GitHub pages, Netlify, etc?
`dioxus publish --ghpages myrepo.git`
# Use in your project
---
Sometimes you'll want to include static assets without bundling them into your .wasm content. dioxus-cli provides a few ways of doing this:
- Load in dynamic content using `dioxus::asset("./static/images/blah.svg")`
- Live-reload HTML templates without rebuilding your .wasm with `dioxus::template("./templates/blah.html")`
- Use a CSS library like tailwind in your dioxus configuration with
+146
View File
@@ -0,0 +1,146 @@
use crate::{
cli::BuildOptions,
config::{Config, ExecutableType},
error::Result,
};
use log::{info, warn};
use std::{io::Write, process::Command};
use wasm_bindgen_cli_support::Bindgen;
pub struct BuildConfig {}
impl Into<BuildConfig> for BuildOptions {
fn into(self) -> BuildConfig {
BuildConfig {}
}
}
impl Default for BuildConfig {
fn default() -> Self {
Self {}
}
}
pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
/*
[1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
[2] Generate the appropriate build folders
[3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
[4] Wasm-opt the .wasm file with whatever optimizations need to be done
[5] Link up the html page to the wasm module
*/
let Config {
out_dir,
crate_dir,
target_dir,
static_dir,
executable,
..
} = config;
let t_start = std::time::Instant::now();
// [1] Build the .wasm module
info!("Running build commands...");
let mut cmd = Command::new("cargo");
cmd.current_dir(&crate_dir)
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if config.release {
cmd.arg("--release");
}
match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let mut child = cmd.spawn()?;
let _err_code = child.wait()?;
info!("Build complete!");
// [2] Establish the output directory structure
let bindgen_outdir = out_dir.join("wasm");
// [3] Bindgen the final binary for use easy linking
let mut bindgen_builder = Bindgen::new();
let release_type = match config.release {
true => "release",
false => "debug",
};
let input_path = match executable {
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
// .join("wasm32-unknown-unknown/release")
.join(format!("wasm32-unknown-unknown/{}", release_type))
.join(format!("{}.wasm", name)),
ExecutableType::Example(name) => target_dir
// .join("wasm32-unknown-unknown/release/examples")
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
.join(format!("{}.wasm", name)),
};
bindgen_builder
.input_path(input_path)
.web(true)?
.debug(true)
.demangle(true)
.keep_debug(true)
.remove_name_section(false)
.remove_producers_section(false)
.out_name("module")
.generate(&bindgen_outdir)?;
// [4]
// TODO: wasm-opt
// [5] Generate the html file with the module name
// TODO: support names via options
info!("Writing to '{:#?}' directory...", out_dir);
let mut file = std::fs::File::create(out_dir.join("index.html"))?;
file.write_all(gen_page("./wasm/module.js").as_str().as_bytes())?;
let copy_options = fs_extra::dir::CopyOptions::new();
match fs_extra::dir::copy(static_dir, out_dir, &copy_options) {
Ok(_) => {}
Err(_e) => {
warn!("Error copying dir");
}
}
let t_end = std::time::Instant::now();
log::info!("Done in {}ms! 🎉", (t_end - t_start).as_millis());
Ok(())
}
fn gen_page(module: &str) -> String {
format!(
r#"
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta charset="UTF-8" />
<link
href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
rel="stylesheet"
/>
</head>
<body>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
import init from "{}";
init("./wasm/module_bg.wasm");
</script>
</body>
</html>
"#,
module
)
}
+69
View File
@@ -0,0 +1,69 @@
//! Utilities for working with cargo and rust files
use crate::error::{Error, Result};
use std::{env, fs, path::PathBuf, process::Command, str};
/// How many parent folders are searched for a `Cargo.toml`
const MAX_ANCESTORS: u32 = 10;
/// Returns the root of the crate that the command is run from
///
/// If the command is run from the workspace root, this will return the top-level Cargo.toml
pub fn crate_root() -> Result<PathBuf> {
// From the current directory we work our way up, looking for `Cargo.toml`
env::current_dir()
.ok()
.and_then(|mut wd| {
for _ in 0..MAX_ANCESTORS {
if contains_manifest(&wd) {
return Some(wd);
}
if !wd.pop() {
break;
}
}
None
})
.ok_or_else(|| Error::CargoError("Failed to find the cargo directory".to_string()))
}
/// Returns the root of a workspace
/// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow)
pub fn workspace_root() -> Result<PathBuf> {
let output = Command::new("cargo")
.args(&["metadata"])
.output()
.map_err(|_| Error::CargoError("Manifset".to_string()))?;
if !output.status.success() {
let mut msg = str::from_utf8(&output.stderr).unwrap().trim();
if msg.starts_with("error: ") {
msg = &msg[7..];
}
return Err(Error::CargoError(msg.to_string()));
}
let stdout = str::from_utf8(&output.stdout).unwrap();
for line in stdout.lines() {
let meta: serde_json::Value = serde_json::from_str(line)
.map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
let root = meta["workspace_root"]
.as_str()
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?;
return Ok(root.into());
}
Err(Error::CargoError("InvalidOutput".to_string()))
}
/// Checks if the directory contains `Cargo.toml`
fn contains_manifest(path: &PathBuf) -> bool {
fs::read_dir(path)
.map(|entries| {
entries
.filter_map(Result::ok)
.any(|ent| &ent.file_name() == "Cargo.toml")
})
.unwrap_or(false)
}
+62
View File
@@ -0,0 +1,62 @@
use argh::FromArgs;
#[derive(FromArgs, PartialEq, Debug)]
/// Top-level command.
pub struct LaunchOptions {
#[argh(subcommand)]
pub command: LaunchCommand,
}
/// The various kinds of commands that `wasm-pack` can execute.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
pub enum LaunchCommand {
Develop(DevelopOptions),
Build(BuildOptions),
Test(TestOptions),
Publish(PublishOptions),
}
/// Publish your yew application to Github Pages, Netlify, or S3
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "publish")]
pub struct PublishOptions {}
/// 🔬 test your yew application!
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "test")]
pub struct TestOptions {
/// an example in the crate
#[argh(option)]
pub example: Option<String>,
}
/// 🏗️ Build your yew application
#[derive(FromArgs, PartialEq, Debug, Clone)]
#[argh(subcommand, name = "build")]
pub struct BuildOptions {
/// an optional direction which is "up" by default
#[argh(option, short = 'o', default = "String::from(\"public\")")]
pub outdir: String,
/// an example in the crate
#[argh(option)]
pub example: Option<String>,
/// develop in release mode
#[argh(switch, short = 'r')]
pub release: bool,
}
/// 🛠 Start a development server
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "develop")]
pub struct DevelopOptions {
/// an example in the crate
#[argh(option)]
pub example: Option<String>,
/// develop in release mode
#[argh(switch, short = 'r')]
pub release: bool,
}
+92
View File
@@ -0,0 +1,92 @@
use crate::{
cli::{BuildOptions, DevelopOptions},
error::Result,
};
use std::{io::Write, path::PathBuf, process::Command};
#[derive(Debug, Clone)]
pub struct Config {
pub out_dir: PathBuf,
pub crate_dir: PathBuf,
pub workspace_dir: PathBuf,
pub target_dir: PathBuf,
pub static_dir: PathBuf,
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
pub executable: ExecutableType,
pub release: bool,
}
#[derive(Debug, Clone)]
pub enum ExecutableType {
Binary(String),
Lib(String),
Example(String),
}
impl Config {
pub fn new() -> Result<Self> {
let crate_dir = crate::cargo::crate_root()?;
let workspace_dir = crate::cargo::workspace_root()?;
let target_dir = workspace_dir.join("target");
let out_dir = crate_dir.join("public");
let cargo_def = &crate_dir.join("Cargo.toml");
let static_dir = crate_dir.join("static");
let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap();
// We just assume they're using a 'main.rs'
// Anyway, we've already parsed the manifest, so it should be easy to change the type
let output_filename = (&manifest)
.lib
.as_ref()
.and_then(|lib| lib.name.clone())
.or_else(|| {
(&manifest)
.package
.as_ref()
.and_then(|pkg| Some(pkg.name.replace("-", "_")))
.clone()
})
.expect("No lib found from cargo metadata");
let executable = ExecutableType::Binary(output_filename);
let release = false;
Ok(Self {
out_dir,
crate_dir,
workspace_dir,
target_dir,
static_dir,
manifest,
executable,
release,
})
}
pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name);
self
}
pub fn with_release(&mut self, release: bool) -> &mut Self {
self.release = release;
self
}
pub fn with_build_options(&mut self, options: &BuildOptions) {
if let Some(name) = &options.example {
self.as_example(name.clone());
}
self.release = options.release;
self.out_dir = options.outdir.clone().into();
}
pub fn with_develop_options(&mut self, options: &DevelopOptions) {
if let Some(name) = &options.example {
self.as_example(name.clone());
}
self.release = options.release;
self.out_dir = tempfile::Builder::new().tempdir().expect("").into_path();
}
}
+111
View File
@@ -0,0 +1,111 @@
use crate::{builder::BuildConfig, cli::DevelopOptions, config::Config, error::Result};
use async_std::prelude::FutureExt;
use async_std::future;
use async_std::prelude::*;
use log::info;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::time::Duration;
pub struct DevelopConfig {}
impl Into<DevelopConfig> for DevelopOptions {
fn into(self) -> DevelopConfig {
DevelopConfig {}
}
}
pub async fn start(config: &Config, _options: &DevelopConfig) -> Result<()> {
log::info!("Starting development server 🚀");
let Config { out_dir, .. } = config;
// Spawn the server onto a seperate task
// This lets the task progress while we handle file updates
let server = async_std::task::spawn(launch_server(out_dir.clone()));
let watcher = async_std::task::spawn(watch_directory(config.clone()));
match server.race(watcher).await {
Err(e) => log::warn!("Error running development server, {:?}", e),
_ => {}
}
Ok(())
}
async fn watch_directory(config: Config) -> Result<()> {
// Create a channel to receive the events.
let (watcher_tx, watcher_rx) = async_std::channel::bounded(100);
// Automatically select the best implementation for your platform.
// You can also access each implementation directly e.g. INotifyWatcher.
let mut watcher: RecommendedWatcher = Watcher::new_immediate(move |res| {
async_std::task::block_on(watcher_tx.send(res));
})
.expect("failed to make watcher");
let src_dir = crate::cargo::crate_root()?;
// Add a path to be watched. All files and directories at that path and
// below will be monitored for changes.
watcher
.watch(src_dir.join("src"), RecursiveMode::Recursive)
.expect("Failed to watch dir");
watcher
.watch(src_dir.join("examples"), RecursiveMode::Recursive)
.expect("Failed to watch dir");
let build_config = BuildConfig::default();
'run: loop {
crate::builder::build(&config, &build_config)?;
// Wait for the message with a debounce
let _msg = watcher_rx
.recv()
.join(future::ready(1_usize).delay(Duration::from_millis(2000)))
.await;
info!("File updated, rebuilding app");
}
Ok(())
}
async fn launch_server(outdir: PathBuf) -> Result<()> {
let _crate_dir = crate::cargo::crate_root()?;
let _workspace_dir = crate::cargo::workspace_root()?;
let mut app = tide::with_state(ServerState::new(outdir.to_owned()));
let p = outdir.display().to_string();
app.at("/")
.get(|req: tide::Request<ServerState>| async move {
log::info!("Connected to development server");
let state = req.state();
Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?)
})
.serve_dir(p)?;
let port = "8080";
let serve_addr = format!("0.0.0.0:{}", port);
// let serve_addr = format!("127.0.0.1:{}", port);
info!("App available at http://{}", serve_addr);
app.listen(serve_addr).await?;
Ok(())
}
/// https://github.com/http-rs/tide/blob/main/examples/state.rs
/// Tide seems to prefer using state instead of injecting into the app closure
/// The app closure needs to be static and
#[derive(Clone)]
struct ServerState {
serv_path: PathBuf,
}
impl ServerState {
fn new(serv_path: PathBuf) -> Self {
Self { serv_path }
}
}
+34
View File
@@ -0,0 +1,34 @@
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
/// Used when errors need to propogate but are too unique to be typed
#[error("{0}")]
Unique(String),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error("Failed to write error")]
FailedToWrite,
#[error("Failed to write error")]
CargoError(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error::Unique(s.to_string())
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Unique(s)
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod builder;
pub mod cargo;
pub mod cli;
pub mod config;
pub mod develop;
pub mod error;
pub mod logging;
pub mod watch;
+49
View File
@@ -0,0 +1,49 @@
use fern::colors::{Color, ColoredLevelConfig};
use log::debug;
pub fn set_up_logging() {
// configure colors for the whole line
let colors_line = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
// we actually don't need to specify the color for debug and info, they are white by default
.info(Color::White)
.debug(Color::White)
// depending on the terminals color scheme, this is the same as the background color
.trace(Color::BrightBlack);
// configure colors for the name of the level.
// since almost all of them are the same as the color for the whole line, we
// just clone `colors_line` and overwrite our changes
let colors_level = colors_line.clone().info(Color::Green);
// here we set up our fern Dispatch
fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{color_line}[{level}{color_line}] {message}\x1B[0m",
color_line = format_args!(
"\x1B[{}m",
colors_line.get_color(&record.level()).to_fg_str()
),
level = colors_level.color(record.level()),
message = message,
));
})
// set the default log level. to filter out verbose log messages from dependencies, set
// this to Warn and overwrite the log level for your crate.
.level(log::LevelFilter::Info)
// .level(log::LevelFilter::Warn)
// change log levels for individual modules. Note: This looks for the record's target
// field which defaults to the module path but can be overwritten with the `target`
// parameter:
// `info!(target="special_target", "This log message is about special_target");`
// .level_for("dioxus", log::LevelFilter::Debug)
// .level_for("dioxus", log::LevelFilter::Info)
// .level_for("pretty_colored", log::LevelFilter::Trace)
// output to stdout
.chain(std::io::stdout())
.apply()
.unwrap();
debug!("finished setting up logging! yay!");
}
+28
View File
@@ -0,0 +1,28 @@
use diopack::cli::{LaunchCommand, LaunchOptions};
use dioxus_cli as diopack;
#[async_std::main]
async fn main() -> diopack::error::Result<()> {
diopack::logging::set_up_logging();
let opts: LaunchOptions = argh::from_env();
let mut config = diopack::config::Config::new()?;
match opts.command {
LaunchCommand::Build(options) => {
config.with_build_options(&options);
diopack::builder::build(&config, &(options.into()))?;
}
LaunchCommand::Develop(options) => {
config.with_develop_options(&options);
diopack::develop::start(&config, &(options.into())).await?;
}
_ => {
todo!("Command not currently implemented");
}
}
Ok(())
}
+11
View File
@@ -0,0 +1,11 @@
pub struct Watcher {}
impl Watcher {
// Spawns a new notify instance that alerts us of changes
pub fn new() -> Self {
Self {}
}
/// Subsribe to the watcher
pub fn subscribe() {}
}