فهرست منبع

wip: move out of dioxus main

Jonathan Kelley 4 سال پیش
کامیت
aebf6678a8
14فایلهای تغییر یافته به همراه696 افزوده شده و 0 حذف شده
  1. 2 0
      .gitignore
  2. 3 0
      .vscode/settings.json
  3. 33 0
      Cargo.toml
  4. 48 0
      README.md
  5. 146 0
      src/builder.rs
  6. 69 0
      src/cargo.rs
  7. 62 0
      src/cli.rs
  8. 92 0
      src/config.rs
  9. 111 0
      src/develop.rs
  10. 34 0
      src/error.rs
  11. 8 0
      src/lib.rs
  12. 49 0
      src/logging.rs
  13. 28 0
      src/main.rs
  14. 11 0
      src/watch.rs

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "rust-analyzer.inlayHints.enable": false
+}

+ 33 - 0
Cargo.toml

@@ -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 - 0
README.md

@@ -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 - 0
src/builder.rs

@@ -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 - 0
src/cargo.rs

@@ -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 - 0
src/cli.rs

@@ -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 - 0
src/config.rs

@@ -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 - 0
src/develop.rs

@@ -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 - 0
src/error.rs

@@ -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 - 0
src/lib.rs

@@ -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 - 0
src/logging.rs

@@ -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 - 0
src/main.rs

@@ -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 - 0
src/watch.rs

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