mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-27 22:50:19 +00:00
Merge pull request #2 from DioxusLabs/jk/overhaul
Overhaul - add auto reloading, status pages, web sockets, move to Tokio/structopt
This commit is contained in:
commit
0fe74f7dcc
34 changed files with 4185 additions and 195 deletions
68
.github/workflows/main.yml
vendored
Normal file
68
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
name: Rust CI
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
# clippy:
|
||||
# name: Clippy
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# profile: minimal
|
||||
# toolchain: stable
|
||||
# override: true
|
||||
# - uses: Swatinem/rust-cache@v1
|
||||
# - run: rustup component add clippy
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: -- -D warnings
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -1,3 +1 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
{}
|
||||
|
|
24
Cargo.toml
24
Cargo.toml
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "dioxus-studio"
|
||||
version = "0.1.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
"license" = "MIT/Apache-2.0"
|
||||
|
@ -14,20 +14,22 @@ log = "0.4.13"
|
|||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
wasm-bindgen-cli-support = "0.2.78"
|
||||
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"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
||||
fs_extra = "1.2.0"
|
||||
cargo_toml = "0.10.0"
|
||||
futures = "0.3.12"
|
||||
notify = "5.0.0-pre.4"
|
||||
rjdebounce = "0.2.1"
|
||||
tempfile = "3.2.0"
|
||||
html_parser = "0.6.2"
|
||||
tui = { version = "0.16.0", features = ["crossterm"] }
|
||||
crossterm = "0.22.1"
|
||||
binary-install = "0.0.2"
|
||||
convert_case = "0.5.0"
|
||||
structopt = "0.3.25"
|
||||
cargo_metadata = "0.14.1"
|
||||
|
||||
[[bin]]
|
||||
|
||||
path = "src/main.rs"
|
||||
name = "dioxus"
|
||||
|
|
11
TODO.md
Normal file
11
TODO.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
asd
|
||||
- [ ] use a TUI for the dev server aspect of things
|
||||
- [ ] allow any index.html
|
||||
- [ ] use wasmopt for production builds
|
||||
- [ ] pass arguments through to `cargo`
|
||||
- [ ] figure out a bundling strategy
|
||||
- [ ] an external configuration
|
||||
|
||||
|
||||
should we just use trunk?
|
20
extension/.eslintrc.js
Normal file
20
extension/.eslintrc.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**@type {import('eslint').Linter.Config} */
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {
|
||||
'semi': [2, "always"],
|
||||
'@typescript-eslint/no-unused-vars': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
}
|
||||
};
|
13
extension/.gitignore
vendored
Normal file
13
extension/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
npm-debug.log
|
||||
Thumbs.db
|
||||
*/node_modules/
|
||||
node_modules/
|
||||
*/out/
|
||||
out/
|
||||
*/.vs/
|
||||
.vs/
|
||||
tsconfig.lsif.json
|
||||
*.lsif
|
||||
*.db
|
||||
*.vsix
|
24
extension/README.md
Normal file
24
extension/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Dioxus VSCode Extension
|
||||
|
||||
![Dioxus Logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png)
|
||||
|
||||
This extension wraps functionality in Dioxus CLI to be used in your editor! Make sure the dioxus-cli is installed before using this extension.
|
||||
|
||||
## Current commands:
|
||||
|
||||
### Convert HTML to RSX
|
||||
Converts a selection of html to valid rsx.
|
||||
|
||||
### Convert HTML to Dioxus Component
|
||||
|
||||
Converts a selection of html to a valid Dioxus component with all SVGs factored out into their own module.
|
||||
|
||||
## packaging
|
||||
|
||||
```
|
||||
$ cd myExtension
|
||||
$ vsce package
|
||||
# myExtension.vsix generated
|
||||
$ vsce publish
|
||||
# <publisherID>.myExtension published to VS Code Marketplace
|
||||
```
|
3025
extension/package-lock.json
generated
Normal file
3025
extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
extension/package.json
Normal file
50
extension/package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "dioxusstudio",
|
||||
"displayName": "dioxusStudio",
|
||||
"description": "Toolkit for IDE support in Dioxus apps",
|
||||
"version": "0.0.1",
|
||||
"publisher": "jkelleyrtp",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/vscode-extension-samples"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.32.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onCommand:extension.htmlToRsx",
|
||||
"onCommand:extension.htmlToComponent"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.htmlToRsx",
|
||||
"title": "Convert HTML to RSX"
|
||||
},
|
||||
{
|
||||
"command": "extension.htmlToComponent",
|
||||
"title": "Convert HTML to Dioxus Component"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.12.0",
|
||||
"@types/vscode": "^1.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.16.0",
|
||||
"@typescript-eslint/parser": "^4.16.0",
|
||||
"eslint": "^7.21.0",
|
||||
"typescript": "^4.3.5"
|
||||
}
|
||||
}
|
57
extension/src/extension.ts
Normal file
57
extension/src/extension.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const htmlToPureRsx = vscode.commands.registerCommand('extension.htmlToRsx', function () {
|
||||
// Get the active text editor
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (editor) {
|
||||
const document = editor.document;
|
||||
const selection = editor.selection;
|
||||
const word = document.getText(selection);
|
||||
|
||||
const child_proc = spawn("dioxus", ["translate", "-t", word]);
|
||||
|
||||
let result = '';
|
||||
child_proc.stdout?.on('data', data => result += data);
|
||||
|
||||
child_proc.on('close', () => {
|
||||
editor.edit(editBuilder => {
|
||||
if (result != '') {
|
||||
editBuilder.replace(selection, result)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const htmlToComponent = vscode.commands.registerCommand('extension.htmlToComponent', function () {
|
||||
// Get the active text editor
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (editor) {
|
||||
const document = editor.document;
|
||||
const selection = editor.selection;
|
||||
const word = document.getText(selection);
|
||||
|
||||
const child_proc = spawn("dioxus", ["translate", "-c", "-t", word]);
|
||||
|
||||
let result = '';
|
||||
child_proc.stdout?.on('data', data => result += data);
|
||||
|
||||
child_proc.on('close', () => {
|
||||
editor.edit(editBuilder => {
|
||||
if (result != '') {
|
||||
editBuilder.replace(selection, result)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
context.subscriptions.push(htmlToPureRsx);
|
||||
context.subscriptions.push(htmlToComponent);
|
||||
}
|
12
extension/tsconfig.json
Normal file
12
extension/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"lib": ["ES2019"],
|
||||
"outDir": "out",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
|
@ -1,25 +1,15 @@
|
|||
use crate::{
|
||||
cli::BuildOptions,
|
||||
config::{Config, ExecutableType},
|
||||
error::Result,
|
||||
config::{CrateConfig, ExecutableType},
|
||||
error::{Error, Result},
|
||||
};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
process::Command,
|
||||
};
|
||||
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<()> {
|
||||
pub fn build(config: &CrateConfig) -> 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
|
||||
|
@ -28,7 +18,7 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
[5] Link up the html page to the wasm module
|
||||
*/
|
||||
|
||||
let Config {
|
||||
let CrateConfig {
|
||||
out_dir,
|
||||
crate_dir,
|
||||
target_dir,
|
||||
|
@ -40,7 +30,7 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
let t_start = std::time::Instant::now();
|
||||
|
||||
// [1] Build the .wasm module
|
||||
info!("Running build commands...");
|
||||
log::info!("Running build commands...");
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.current_dir(&crate_dir)
|
||||
.arg("build")
|
||||
|
@ -60,9 +50,16 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
};
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
let _err_code = child.wait()?;
|
||||
let output = child.wait()?;
|
||||
|
||||
info!("Build complete!");
|
||||
if output.success() {
|
||||
log::info!("Build complete!");
|
||||
} else {
|
||||
log::error!("Build failed!");
|
||||
let mut reason = String::new();
|
||||
child.stderr.unwrap().read_to_string(&mut reason)?;
|
||||
return Err(Error::BuildFailed(reason));
|
||||
}
|
||||
|
||||
// [2] Establish the output directory structure
|
||||
let bindgen_outdir = out_dir.join("wasm");
|
||||
|
@ -77,12 +74,10 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
|
||||
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)),
|
||||
};
|
||||
|
@ -103,7 +98,7 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
|
||||
// [5] Generate the html file with the module name
|
||||
// TODO: support names via options
|
||||
info!("Writing to '{:#?}' directory...", out_dir);
|
||||
log::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())?;
|
||||
|
||||
|
@ -111,7 +106,7 @@ pub fn build(config: &Config, _build_config: &BuildConfig) -> Result<()> {
|
|||
match fs_extra::dir::copy(static_dir, out_dir, ©_options) {
|
||||
Ok(_) => {}
|
||||
Err(_e) => {
|
||||
warn!("Error copying dir");
|
||||
log::warn!("Error copying dir");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,6 +124,8 @@ fn gen_page(module: &str) -> String {
|
|||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
</div>
|
||||
<!-- Note the usage of `type=module` here as this is an ES6 module -->
|
||||
<script type="module">
|
||||
import init from "{}";
|
||||
|
@ -141,3 +138,35 @@ fn gen_page(module: &str) -> String {
|
|||
module
|
||||
)
|
||||
}
|
||||
|
||||
// use binary_install::{Cache, Download};
|
||||
|
||||
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
|
||||
// /// precompiled binary.
|
||||
// ///
|
||||
// /// Returns `Some` if a binary was found or it was successfully downloaded.
|
||||
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
|
||||
// /// have precompiled binaries. Returns an error if we failed to download the
|
||||
// /// binary.
|
||||
// pub fn find_wasm_opt(
|
||||
// cache: &Cache,
|
||||
// install_permitted: bool,
|
||||
// ) -> Result<install::Status, failure::Error> {
|
||||
// // First attempt to look up in PATH. If found assume it works.
|
||||
// if let Ok(path) = which::which("wasm-opt") {
|
||||
// PBAR.info(&format!("found wasm-opt at {:?}", path));
|
||||
|
||||
// match path.as_path().parent() {
|
||||
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
|
||||
// None => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// let version = "version_78";
|
||||
// Ok(install::download_prebuilt(
|
||||
// &install::Tool::WasmOpt,
|
||||
// cache,
|
||||
// version,
|
||||
// install_permitted,
|
||||
// )?)
|
||||
// }
|
||||
|
|
49
src/cli.rs
49
src/cli.rs
|
@ -8,22 +8,24 @@ pub struct LaunchOptions {
|
|||
}
|
||||
|
||||
/// The various kinds of commands that `wasm-pack` can execute.
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand)]
|
||||
pub enum LaunchCommand {
|
||||
Develop(DevelopOptions),
|
||||
Build(BuildOptions),
|
||||
Translate(TranslateOptions),
|
||||
Test(TestOptions),
|
||||
Publish(PublishOptions),
|
||||
Studio(StudioOptions),
|
||||
}
|
||||
|
||||
/// Publish your yew application to Github Pages, Netlify, or S3
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "publish")]
|
||||
pub struct PublishOptions {}
|
||||
|
||||
/// 🔬 test your yew application!
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "test")]
|
||||
pub struct TestOptions {
|
||||
/// an example in the crate
|
||||
|
@ -35,7 +37,7 @@ pub struct TestOptions {
|
|||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "build")]
|
||||
pub struct BuildOptions {
|
||||
/// an optional direction which is "up" by default
|
||||
/// the directory output
|
||||
#[argh(option, short = 'o', default = "String::from(\"public\")")]
|
||||
pub outdir: String,
|
||||
|
||||
|
@ -46,10 +48,18 @@ pub struct BuildOptions {
|
|||
/// develop in release mode
|
||||
#[argh(switch, short = 'r')]
|
||||
pub release: bool,
|
||||
|
||||
/// hydrate the `dioxusroot` element with this content
|
||||
#[argh(option, short = 'h')]
|
||||
pub hydrate: Option<String>,
|
||||
|
||||
/// custom template
|
||||
#[argh(option, short = 't')]
|
||||
pub template: Option<String>,
|
||||
}
|
||||
|
||||
/// 🛠 Start a development server
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "develop")]
|
||||
pub struct DevelopOptions {
|
||||
/// an example in the crate
|
||||
|
@ -59,4 +69,33 @@ pub struct DevelopOptions {
|
|||
/// develop in release mode
|
||||
#[argh(switch, short = 'r')]
|
||||
pub release: bool,
|
||||
|
||||
/// hydrate the `dioxusroot` element with this content
|
||||
#[argh(option, short = 'h')]
|
||||
pub hydrate: Option<String>,
|
||||
|
||||
/// custom template
|
||||
#[argh(option, short = 't')]
|
||||
pub template: Option<String>,
|
||||
}
|
||||
|
||||
/// 🛠 Translate some 3rd party template into rsx
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "translate")]
|
||||
pub struct TranslateOptions {
|
||||
/// an example in the crate
|
||||
#[argh(option, short = 'f')]
|
||||
pub file: Option<String>,
|
||||
|
||||
/// an example in the crate
|
||||
#[argh(option, short = 't')]
|
||||
pub text: Option<String>,
|
||||
|
||||
/// whether or not to jump
|
||||
#[argh(switch, short = 'c')]
|
||||
pub component: bool,
|
||||
}
|
||||
/// 🛠 Translate some 3rd party template into rsx
|
||||
#[derive(FromArgs, PartialEq, Debug, Clone)]
|
||||
#[argh(subcommand, name = "studio")]
|
||||
pub struct StudioOptions {}
|
||||
|
|
0
src/cmd/build.rs
Normal file
0
src/cmd/build.rs
Normal file
0
src/cmd/mod.rs
Normal file
0
src/cmd/mod.rs
Normal file
0
src/cmd/serve.rs
Normal file
0
src/cmd/serve.rs
Normal file
0
src/cmd/studio.rs
Normal file
0
src/cmd/studio.rs
Normal file
|
@ -1,11 +1,12 @@
|
|||
use crate::{
|
||||
cli::{BuildOptions, DevelopOptions},
|
||||
error::Result,
|
||||
LaunchCommand,
|
||||
};
|
||||
use std::{io::Write, path::PathBuf, process::Command};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub struct CrateConfig {
|
||||
pub out_dir: PathBuf,
|
||||
pub crate_dir: PathBuf,
|
||||
pub workspace_dir: PathBuf,
|
||||
|
@ -23,7 +24,7 @@ pub enum ExecutableType {
|
|||
Example(String),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
impl CrateConfig {
|
||||
pub fn new() -> Result<Self> {
|
||||
let crate_dir = crate::cargo::crate_root()?;
|
||||
let workspace_dir = crate::cargo::workspace_root()?;
|
||||
|
|
111
src/develop.rs
111
src/develop.rs
|
@ -1,111 +0,0 @@
|
|||
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(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 }
|
||||
}
|
||||
}
|
148
src/develop/develop.rs
Normal file
148
src/develop/develop.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use crate::{cli::DevelopOptions, config::CrateConfig, error::Result};
|
||||
use async_std::prelude::FutureExt;
|
||||
|
||||
use log::info;
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use tide::http::mime::HTML;
|
||||
use tide::http::Mime;
|
||||
|
||||
pub struct DevelopState {
|
||||
//
|
||||
reload_on_change: bool,
|
||||
}
|
||||
|
||||
pub async fn develop(options: DevelopOptions) -> Result<()> {
|
||||
//
|
||||
log::info!("Starting development server 🚀");
|
||||
let mut cfg = CrateConfig::new()?;
|
||||
cfg.with_develop_options(&options);
|
||||
|
||||
let out_dir = cfg.out_dir.clone();
|
||||
|
||||
let is_err = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// 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, is_err.clone()));
|
||||
let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone()));
|
||||
|
||||
match server.race(watcher).await {
|
||||
Err(e) => log::warn!("Error running development server, {:?}", e),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> 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(move |res| {
|
||||
<<<<<<< HEAD
|
||||
async_std::task::block_on(watcher_tx.send(res));
|
||||
=======
|
||||
// send an event
|
||||
let _ = async_std::task::block_on(watcher_tx.send(res));
|
||||
>>>>>>> 9451713 (wip: studio upgrades)
|
||||
})
|
||||
.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");
|
||||
|
||||
match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::warn!("Failed to watch examples dir, {:?}", e),
|
||||
}
|
||||
|
||||
'run: loop {
|
||||
match crate::builder::build(&config) {
|
||||
Ok(_) => {
|
||||
is_err.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed),
|
||||
};
|
||||
|
||||
let mut msg = None;
|
||||
loop {
|
||||
let new_msg = watcher_rx.recv().await.unwrap().unwrap();
|
||||
if !watcher_rx.is_empty() {
|
||||
msg = Some(new_msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info!("File updated, rebuilding app");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> 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(), is_err));
|
||||
|
||||
let file_path = format!("{}/index.html", outdir.display());
|
||||
log::info!("Serving {}", file_path);
|
||||
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();
|
||||
|
||||
match state.is_err.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
true => {
|
||||
//
|
||||
let mut resp =
|
||||
tide::Body::from_string(format!(include_str!("../err.html"), err = "_"));
|
||||
resp.set_mime(HTML);
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
false => {
|
||||
Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?)
|
||||
}
|
||||
}
|
||||
})
|
||||
.serve_dir(p)?;
|
||||
// .serve_file(file_path)
|
||||
// .unwrap();
|
||||
|
||||
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,
|
||||
is_err: ErrStatus,
|
||||
}
|
||||
|
||||
type ErrStatus = Arc<AtomicBool>;
|
||||
|
||||
impl ServerState {
|
||||
fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self {
|
||||
Self { serv_path, is_err }
|
||||
}
|
||||
}
|
0
src/develop/draw.rs
Normal file
0
src/develop/draw.rs
Normal file
0
src/develop/events.rs
Normal file
0
src/develop/events.rs
Normal file
164
src/develop/studio.rs
Normal file
164
src/develop/studio.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
//! It's better to store all the configuration in one spot
|
||||
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
widgets::{
|
||||
Axis, BarChart, Block, BorderType, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List,
|
||||
ListItem, Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
use std::{any::Any, io::Write, path::PathBuf, process::Command};
|
||||
|
||||
pub struct Cfg {
|
||||
command: LaunchOptions,
|
||||
headless: bool,
|
||||
example: Option<String>,
|
||||
outdir: Option<String>,
|
||||
release: bool,
|
||||
hydrate: Option<String>,
|
||||
template: Option<String>,
|
||||
translate_file: Option<String>,
|
||||
crate_config: Option<CrateConfig>,
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
pub async fn start(options: DevelopOptions) -> Result<()> {
|
||||
let mut state = Cfg {
|
||||
command: todo!(),
|
||||
headless: todo!(),
|
||||
example: todo!(),
|
||||
outdir: todo!(),
|
||||
release: todo!(),
|
||||
hydrate: todo!(),
|
||||
template: todo!(),
|
||||
translate_file: todo!(),
|
||||
crate_config: todo!(),
|
||||
should_quit: false,
|
||||
};
|
||||
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(std::io::stdout());
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// Setup input handling
|
||||
// let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let tick_rate = std::time::Duration::from_millis(100);
|
||||
|
||||
let mut prev_time = std::time::Instant::now();
|
||||
while !state.should_quit {
|
||||
let next_time = prev_time + tick_rate;
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let diff = next_time - std::time::Instant::now();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TuiStudio {
|
||||
cfg: Cfg,
|
||||
hook_idx: usize,
|
||||
hooks: Vec<Box<dyn Any>>,
|
||||
}
|
||||
impl TuiStudio {
|
||||
fn use_hook<F: 'static>(&mut self, f: impl FnOnce() -> F) -> &mut F {
|
||||
if self.hook_idx == self.hooks.len() {
|
||||
self.hooks.push(Box::new(f()));
|
||||
}
|
||||
let idx = self.hook_idx;
|
||||
self.hook_idx += 1;
|
||||
let hook = self.hooks.get_mut(idx).unwrap();
|
||||
let r = hook.downcast_mut::<F>().unwrap();
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStudio {
|
||||
fn event_handler(&self, action: crossterm::event::Event) -> anyhow::Result<()> {
|
||||
match action {
|
||||
crossterm::event::Event::Key(_) => {}
|
||||
crossterm::event::Event::Mouse(_) => {}
|
||||
crossterm::event::Event::Resize(_, _) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) {}
|
||||
|
||||
fn tick(&mut self) {}
|
||||
|
||||
fn should_quit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<B: tui::backend::Backend>(&mut self, f: &mut tui::Frame<B>) {
|
||||
self.hook_idx = 0;
|
||||
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
let size = f.size();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
let block = Block::default()
|
||||
.title(vec![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
let block = Block::default().title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStudio {
|
||||
fn render_list<B: tui::backend::Backend>(&mut self, f: &mut tui::Frame<B>) {
|
||||
let options = [
|
||||
"Bundle", "Develop",
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
19
src/err.html
Normal file
19
src/err.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<html>
|
||||
|
||||
<head></head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>
|
||||
Sorry, but building your application failed.
|
||||
</h1>
|
||||
<p>
|
||||
Here's the error:
|
||||
</p>
|
||||
<code>
|
||||
{err}
|
||||
</code>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -14,6 +14,9 @@ pub enum Error {
|
|||
#[error("Failed to write error")]
|
||||
FailedToWrite,
|
||||
|
||||
#[error("Building project failed")]
|
||||
BuildFailed(String),
|
||||
|
||||
#[error("Failed to write error")]
|
||||
CargoError(String),
|
||||
|
||||
|
|
0
src/helpers/extract_svgs.rs
Normal file
0
src/helpers/extract_svgs.rs
Normal file
181
src/helpers/to_component.rs
Normal file
181
src/helpers/to_component.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
//! Intelligently converts html to rsx with appropraite transformations and extractions.
|
||||
//!
|
||||
//! - [*] Creates a component
|
||||
//! - [ ] Extracts svgs
|
||||
//! - [ ] Attempts to extract lists
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use html_parser::{Dom, Element, Node};
|
||||
|
||||
pub fn convert_html_to_component(html: &str) -> Result<ComponentRenderer> {
|
||||
Ok(ComponentRenderer {
|
||||
dom: Dom::parse(html)?,
|
||||
icon_index: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ComponentRenderer {
|
||||
dom: Dom,
|
||||
icon_index: usize,
|
||||
}
|
||||
|
||||
impl Display for ComponentRenderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
r##"
|
||||
fn component(cx: Scope) -> Element {{
|
||||
cx.render(rsx!("##
|
||||
)?;
|
||||
let mut svg_nodes = vec![];
|
||||
|
||||
let mut svg_idx = 0;
|
||||
for child in &self.dom.children {
|
||||
render_child(f, child, 2, &mut svg_nodes, true, &mut svg_idx)?;
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
r##" ))
|
||||
}}"##
|
||||
)?;
|
||||
|
||||
if svg_idx == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
writeln!(f, "\n\nmod icons {{")?;
|
||||
|
||||
let mut id = 0;
|
||||
while let Some(svg) = svg_nodes.pop() {
|
||||
writeln!(
|
||||
f,
|
||||
r##" pub(super) fn icon_{}(cx: Scope) -> Element {{
|
||||
cx.render(rsx!("##,
|
||||
id
|
||||
)?;
|
||||
write_tabs(f, 3)?;
|
||||
|
||||
render_element(f, svg, 3, &mut svg_nodes, false, &mut 0)?;
|
||||
writeln!(f, "\t\t))\n\t}}\n")?;
|
||||
id += 1;
|
||||
}
|
||||
|
||||
writeln!(f, "}}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_child<'a>(
|
||||
f: &mut Formatter<'_>,
|
||||
child: &'a Node,
|
||||
il: u32,
|
||||
svg_buffer: &mut Vec<&'a Element>,
|
||||
skip_svg: bool,
|
||||
svg_idx: &mut usize,
|
||||
) -> std::fmt::Result {
|
||||
write_tabs(f, il)?;
|
||||
match child {
|
||||
Node::Text(t) => writeln!(f, "\"{}\"", t)?,
|
||||
Node::Comment(e) => writeln!(f, "/* {} */", e)?,
|
||||
Node::Element(el) => render_element(f, el, il, svg_buffer, skip_svg, svg_idx)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_element<'a>(
|
||||
f: &mut Formatter<'_>,
|
||||
el: &'a Element,
|
||||
il: u32,
|
||||
svg_buffer: &mut Vec<&'a Element>,
|
||||
skip_svg: bool,
|
||||
svg_idx: &mut usize,
|
||||
) -> std::fmt::Result {
|
||||
if el.name == "svg" && skip_svg {
|
||||
svg_buffer.push(el);
|
||||
// todo: attach the right icon ID
|
||||
writeln!(f, "icons::icon_{} {{}}", svg_idx)?;
|
||||
*svg_idx += 1;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// open the tag
|
||||
write!(f, "{} {{ ", &el.name)?;
|
||||
|
||||
// todo: dioxus will eventually support classnames
|
||||
// for now, just write them with a space between each
|
||||
let class_iter = &mut el.classes.iter();
|
||||
if let Some(first_class) = class_iter.next() {
|
||||
write!(f, "class: \"{}", first_class)?;
|
||||
for next_class in class_iter {
|
||||
write!(f, " {}", next_class)?;
|
||||
}
|
||||
write!(f, "\",")?;
|
||||
}
|
||||
write!(f, "\n")?;
|
||||
|
||||
// write the attributes
|
||||
if let Some(id) = &el.id {
|
||||
write_tabs(f, il + 1)?;
|
||||
writeln!(f, "id: \"{}\",", id)?;
|
||||
}
|
||||
|
||||
for (name, value) in &el.attributes {
|
||||
write_tabs(f, il + 1)?;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
if name.chars().any(|ch| ch.is_ascii_uppercase() || ch == '-') {
|
||||
let new_name = name.to_case(Case::Snake);
|
||||
match value {
|
||||
Some(val) => writeln!(f, "{}: \"{}\",", new_name, val)?,
|
||||
None => writeln!(f, "{}: \"\",", new_name)?,
|
||||
}
|
||||
} else {
|
||||
match name.as_str() {
|
||||
"for" | "async" | "type" | "as" => write!(f, "r#")?,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match value {
|
||||
Some(val) => writeln!(f, "{}: \"{}\",", name, val)?,
|
||||
None => writeln!(f, "{}: \"\",", name)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now the children
|
||||
for child in &el.children {
|
||||
render_child(f, child, il + 1, svg_buffer, skip_svg, svg_idx)?;
|
||||
}
|
||||
|
||||
// close the tag
|
||||
write_tabs(f, il)?;
|
||||
writeln!(f, "}}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_tabs(f: &mut Formatter, num: u32) -> std::fmt::Result {
|
||||
for _ in 0..num {
|
||||
write!(f, " ")?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_svgs() {
|
||||
let st = include_str!("../../tests/svg.html");
|
||||
|
||||
let out = format!("{:}", convert_html_to_component(st).unwrap());
|
||||
dbg!(&out);
|
||||
|
||||
std::fs::File::create("svg_rsx.rs")
|
||||
.unwrap()
|
||||
.write_all(out.as_bytes())
|
||||
.unwrap();
|
||||
}
|
128
src/helpers/translate.rs
Normal file
128
src/helpers/translate.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use html_parser::{Dom, Node};
|
||||
|
||||
pub fn translate_from_html_file(target: &str) -> Result<RsxRenderer> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
let mut file = File::open(target).unwrap();
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)
|
||||
.expect("Failed to read your file.");
|
||||
translate_from_html_to_rsx(&contents, true)
|
||||
}
|
||||
|
||||
pub fn translate_from_html_to_rsx(html: &str, as_call: bool) -> Result<RsxRenderer> {
|
||||
let contents = Dom::parse(html)?;
|
||||
let renderer = RsxRenderer {
|
||||
as_call,
|
||||
dom: contents,
|
||||
};
|
||||
Ok(renderer)
|
||||
}
|
||||
|
||||
pub struct RsxRenderer {
|
||||
dom: Dom,
|
||||
as_call: bool,
|
||||
}
|
||||
|
||||
impl Display for RsxRenderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.as_call {
|
||||
writeln!(f, r##"use dioxus::prelude::*;"##)?;
|
||||
writeln!(
|
||||
f,
|
||||
r##"
|
||||
fn component(cx: Scope) -> Element {{
|
||||
cx.render(rsx!(
|
||||
"##
|
||||
)?;
|
||||
}
|
||||
for child in &self.dom.children {
|
||||
render_child(f, child, 1)?;
|
||||
}
|
||||
if self.as_call {
|
||||
write!(
|
||||
f,
|
||||
r##"
|
||||
)
|
||||
)
|
||||
"##
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_child(f: &mut Formatter<'_>, child: &Node, il: u32) -> std::fmt::Result {
|
||||
write_tabs(f, il);
|
||||
match child {
|
||||
Node::Text(t) => writeln!(f, "\"{}\"", t)?,
|
||||
Node::Comment(e) => writeln!(f, "/* {} */", e)?,
|
||||
Node::Element(el) => {
|
||||
// open the tag
|
||||
write!(f, "{} {{ ", &el.name)?;
|
||||
|
||||
// todo: dioxus will eventually support classnames
|
||||
// for now, just write them with a space between each
|
||||
let class_iter = &mut el.classes.iter();
|
||||
if let Some(first_class) = class_iter.next() {
|
||||
write!(f, "class: \"{}", first_class)?;
|
||||
for next_class in class_iter {
|
||||
write!(f, " {}", next_class)?;
|
||||
}
|
||||
write!(f, "\",")?;
|
||||
}
|
||||
write!(f, "\n")?;
|
||||
|
||||
// write the attributes
|
||||
if let Some(id) = &el.id {
|
||||
write_tabs(f, il + 1)?;
|
||||
writeln!(f, "id: \"{}\",", id)?;
|
||||
}
|
||||
|
||||
for (name, value) in &el.attributes {
|
||||
write_tabs(f, il + 1)?;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
if name.chars().any(|ch| ch.is_ascii_uppercase() || ch == '-') {
|
||||
let new_name = name.to_case(Case::Snake);
|
||||
match value {
|
||||
Some(val) => writeln!(f, "{}: \"{}\",", new_name, val)?,
|
||||
None => writeln!(f, "{}: \"\",", new_name)?,
|
||||
}
|
||||
} else {
|
||||
match name.as_str() {
|
||||
"for" | "async" | "type" | "as" => write!(f, "r#")?,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match value {
|
||||
Some(val) => writeln!(f, "{}: \"{}\",", name, val)?,
|
||||
None => writeln!(f, "{}: \"\",", name)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now the children
|
||||
for child in &el.children {
|
||||
render_child(f, child, il + 1)?;
|
||||
}
|
||||
|
||||
// close the tag
|
||||
write_tabs(f, il)?;
|
||||
writeln!(f, "}}")?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_tabs(f: &mut Formatter, num: u32) -> std::fmt::Result {
|
||||
for _ in 0..num {
|
||||
write!(f, " ")?
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
pub mod builder;
|
||||
pub mod cargo;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod develop;
|
||||
pub mod error;
|
||||
pub mod logging;
|
||||
pub mod watch;
|
|
@ -1,5 +1,4 @@
|
|||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use log::debug;
|
||||
|
||||
pub fn set_up_logging() {
|
||||
// configure colors for the whole line
|
||||
|
@ -29,21 +28,8 @@ pub fn set_up_logging() {
|
|||
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!");
|
||||
}
|
||||
|
|
101
src/main.rs
101
src/main.rs
|
@ -1,28 +1,97 @@
|
|||
use dioxus_studio as diopack;
|
||||
use dioxus_studio::cli::{LaunchCommand, LaunchOptions};
|
||||
mod builder;
|
||||
mod cargo;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod error;
|
||||
mod logging;
|
||||
mod helpers {
|
||||
pub mod extract_svgs;
|
||||
pub mod to_component;
|
||||
pub mod translate;
|
||||
}
|
||||
|
||||
mod watch;
|
||||
mod develop {
|
||||
pub mod develop;
|
||||
pub mod draw;
|
||||
pub mod events;
|
||||
pub mod studio;
|
||||
}
|
||||
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> diopack::error::Result<()> {
|
||||
diopack::logging::set_up_logging();
|
||||
async fn main() -> Result<()> {
|
||||
set_up_logging();
|
||||
let args = Args::from_args();
|
||||
|
||||
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()))?;
|
||||
match args.command {
|
||||
LaunchCommand::Develop(cfg) => {
|
||||
develop::develop::develop(cfg).await?;
|
||||
}
|
||||
// LaunchCommand::Develop(cfg) => develop::studio::start(cfg).await?,
|
||||
LaunchCommand::Build(opts) => {
|
||||
let mut cfg = CrateConfig::new()?;
|
||||
cfg.with_build_options(&opts);
|
||||
builder::build(&cfg)?;
|
||||
}
|
||||
|
||||
LaunchCommand::Develop(options) => {
|
||||
config.with_develop_options(&options);
|
||||
diopack::develop::start(&config, &(options.into())).await?;
|
||||
}
|
||||
LaunchCommand::Translate(cfg) => {
|
||||
let TranslateOptions {
|
||||
file,
|
||||
text,
|
||||
component,
|
||||
} = cfg;
|
||||
|
||||
match component {
|
||||
true => {
|
||||
let f = helpers::to_component::convert_html_to_component(&text.unwrap())?;
|
||||
println!("{}", f);
|
||||
}
|
||||
false => {
|
||||
let renderer = match (file, text) {
|
||||
(None, Some(text)) => translate::translate_from_html_to_rsx(&text, false)?,
|
||||
(Some(file), None) => translate::translate_from_html_file(&file)?,
|
||||
_ => panic!("Must select either file or text - not both or none!"),
|
||||
};
|
||||
|
||||
println!("{}", renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
todo!("Command not currently implemented");
|
||||
todo!("Those commands are not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build, bundle & ship your Rust WASM application to the web.
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(name = "trunk")]
|
||||
struct Args {
|
||||
#[structopt(subcommand)]
|
||||
command: TrunkSubcommands,
|
||||
/// Path to the Trunk config file [default: Trunk.toml]
|
||||
#[structopt(long, parse(from_os_str), env = "TRUNK_CONFIG")]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[structopt(short)]
|
||||
pub v: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum TrunkSubcommands {
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
Build(cmd::build::Build),
|
||||
/// Build & watch the Rust WASM app and all of its assets.
|
||||
Watch(cmd::watch::Watch),
|
||||
/// Build, watch & serve the Rust WASM app and all of its assets.
|
||||
Serve(cmd::serve::Serve),
|
||||
/// Clean output artifacts.
|
||||
Clean(cmd::clean::Clean),
|
||||
/// Trunk config controls.
|
||||
Config(cmd::config::Config),
|
||||
}
|
||||
|
|
30
tests/svg.html
Normal file
30
tests/svg.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<div>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
32
tests/test.html
Normal file
32
tests/test.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<section class="text-gray-600 body-font">
|
||||
<div class="container px-5 py-24 mx-auto">
|
||||
<div class="flex flex-wrap -mx-4 -mb-10 text-center">
|
||||
<div class="sm:w-1/2 mb-10 px-4">
|
||||
<div class="rounded-lg h-64 overflow-hidden">
|
||||
<img alt="content" class="object-cover object-center h-full w-full"
|
||||
src="https://dummyimage.com/1201x501">
|
||||
</div>
|
||||
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">Buy YouTube Videos</h2>
|
||||
<p class="leading-relaxed text-base">
|
||||
Williamsburg occupy sustainable snackwave gochujang. Pinterest cornhole brunch, slow-carb neutra
|
||||
irony.
|
||||
</p>
|
||||
<button
|
||||
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded">Button</button>
|
||||
</div>
|
||||
<div class="sm:w-1/2 mb-10 px-4">
|
||||
<div class="rounded-lg h-64 overflow-hidden">
|
||||
<img alt="content" class="object-cover object-center h-full w-full"
|
||||
src="https://dummyimage.com/1202x502">
|
||||
</div>
|
||||
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">The Catalyzer</h2>
|
||||
<p class="leading-relaxed text-base">
|
||||
Williamsburg occupy sustainable snackwave gochujang. Pinterest
|
||||
cornhole brunch, slow-carb neutra irony.
|
||||
</p>
|
||||
<button
|
||||
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded">Button</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
Loading…
Reference in a new issue