Explorar el Código

Feat: add diopack

Jonathan Kelley hace 4 años
padre
commit
23ab5af1bf

+ 1 - 0
Cargo.toml

@@ -7,6 +7,7 @@ members = [
     "packages/recoil",
     "packages/redux",
     "packages/web",
+    "packages/diopack",
     #
     #
     #

+ 5 - 2
README.md

@@ -59,9 +59,12 @@ async fn user_data(ctx: &Context<()>) -> VNode {
 Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components. 
 
 ## Examples
-We use `diopack` to build and test apps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. Alternatively, `Trunk` works but can't run examples.
+We use `diopack` to build and test webapps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/diopack` will get it installed. 
 
-- tide_ssr: Handle an HTTP request and return an html body using the html! macro.
+Alternatively, `Trunk` works but can't run examples.
+
+- tide_ssr: Handle an HTTP request and return an html body using the html! macro. `cargo run --example tide_ssr`
+- simple_wasm: Simple WASM app that says hello. `diopack develop --example simple`
 
 ## Documentation
 We have a pretty robust 

+ 2 - 0
packages/diopack/.gitignore

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

+ 3 - 0
packages/diopack/.vscode/settings.json

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

+ 26 - 0
packages/diopack/Cargo.toml

@@ -0,0 +1,26 @@
+[package]
+name = "diopack"
+version = "0.0.0"
+authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+thiserror = "1.0.22"
+log = "0.4"
+fern = { version = "0.6.0", features = ["colored"] }
+wasm-bindgen-cli-support = "0.2.69"
+anyhow = "1.0.34"
+argh = "0.1.4"
+serde = "1"
+serde_json = "1"
+async-std = { version = "1.7.0", features = ["attributes"] }
+tide = "0.15.0"
+cargo_toml = "0.8.1"
+fs_extra = "1.2.0"
+
+notify = "5.0.0-pre.3"
+futures = "0.3.8"
+rjdebounce = "0.2.1"
+tempfile = "3.1.0"

+ 48 - 0
packages/diopack/README.md

@@ -0,0 +1,48 @@
+<div align="center">
+  <h1>📦✨  yew-pack</h1>
+  <p>
+    <strong>Tooling to supercharge yew projects</strong>
+  </p>
+</div>
+
+# About
+---
+yewpack (inspired by wasm-pack and webpack) is a tool to help get Yew 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 `yewpack publish`.
+
+Best thing: 
+- No NPM. 
+- No Webpack. 
+- No `node_modules`. 
+- No Babel
+- No parcel
+- No rollup
+- No ESLint
+
+Just install Rust, yewpack, and you're good to go.
+`cargo install --git github.com/jkelleyrtp/yewpack`
+
+Need a development server?
+`yewpack develop`
+
+Need to run an example?
+`yewpack develop --example textbox`
+
+Need to benchmark a component?
+`yewpack bench`
+
+Need to test your code?
+`yewpack test`
+
+Need to build your code into a bundle?
+`yewpack build --outdir public`
+
+Need to publish your code to GitHub pages, Netlify, etc?
+`yewpack publish --ghpages myrepo.git`
+
+# Use in your project
+---
+Sometimes you'll want to include static assets without bundling them into your .wasm content. yewpack provides a few ways of doing this:
+
+- Load in dynamic content using `yewpack::asset("./static/images/blah.svg")`
+- Live-reload HTML templates without rebuilding your .wasm with `yewpack::template("./templates/blah.html")`
+- Use a CSS library like tailwind in your yewpack configuration with

+ 134 - 0
packages/diopack/src/builder.rs

@@ -0,0 +1,134 @@
+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("--release")
+        .arg("--target")
+        .arg("wasm32-unknown-unknown")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped());
+
+    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()?;
+
+    // [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 input_path = match executable {
+        ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
+            .join("wasm32-unknown-unknown/release")
+            .join(format!("{}.wasm", name)),
+
+        ExecutableType::Example(name) => target_dir
+            .join("wasm32-unknown-unknown/release/examples")
+            .join(format!("{}.wasm", name)),
+    };
+
+    bindgen_builder
+        .input_path(input_path)
+        .web(true)?
+        .debug(true)
+        .demangle(true)
+        .keep_debug(true)
+        .remove_name_section(true)
+        .remove_producers_section(true)
+        .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();
+    </script>
+  </body>
+</html>
+"#,
+        module
+    )
+}

+ 69 - 0
packages/diopack/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)
+}

+ 55 - 0
packages/diopack/src/cli.rs

@@ -0,0 +1,55 @@
+
+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>,
+}
+
+/// 🛠 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>,
+}

+ 83 - 0
packages/diopack/src/config.rs

@@ -0,0 +1,83 @@
+use crate::{
+    cli::{BuildOptions, DevelopOptions},
+    error::Result,
+};
+// use log::{info, warn};
+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,
+}
+
+#[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);
+
+        Ok(Self {
+            out_dir,
+            crate_dir,
+            workspace_dir,
+            target_dir,
+            static_dir,
+            manifest,
+            executable,
+        })
+    }
+
+    pub fn as_example(&mut self, example_name: String) -> &mut Self {
+        self.executable = ExecutableType::Example(example_name);
+        self
+    }
+
+    pub fn with_build_options(&mut self, options: &BuildOptions) {
+        if let Some(name) = &options.example {
+            self.as_example(name.clone());
+        }
+        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());
+        }
+        let outdir = tempfile::Builder::new().tempdir().expect("").into_path();
+        self.out_dir = outdir;
+    }
+}

+ 110 - 0
packages/diopack/src/develop.rs

@@ -0,0 +1,110 @@
+use crate::{builder::BuildConfig, cli::DevelopOptions, config::Config, error::Result};
+use async_std::{channel, 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!("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
packages/diopack/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
packages/diopack/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;

+ 47 - 0
packages/diopack/src/logging.rs

@@ -0,0 +1,47 @@
+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::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("diopack", 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!");
+}

+ 27 - 0
packages/diopack/src/main.rs

@@ -0,0 +1,27 @@
+use diopack::cli::{LaunchCommand, LaunchOptions};
+
+#[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
packages/diopack/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() {}
+}