From 4682b9d3e2cf542c6ac521ee3563c4e34c342a8b Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Mon, 18 Jul 2022 11:50:58 +1000 Subject: [PATCH] feat: made cli auto-install needed tools and use global flags for config (#160) * feat(cli): made cli use `wasm-bindgen` with full binary structure rather than `wasm-pack` This provides *significant* flexibility for all sorts of brilliant new features! * refactor: moved target directories into `dist/` This cleans things up a little, and makes directory structures and cleaning build artifacts much simpler. * feat(cli): made `wasm-bindgen` and `wasm-opt` automatically install locally Perseus now has no dependencies except Rust! This also *dramatically* reduces bundle sizes for some reason (`wasm-pack` overhead?), by over 50kb in the basic example. * fix(deployment): fixed deployed binaries not finding artifacts * fix(deployment): fixed deployment copying GBs of cargo assets Since we now store `cargo` target directories inside `dist/`, they were being copied for exporting! * feat(cli): made `wasm32-unknown-unknown` target auto-install with `rustup` available * refactor(examples): updated all examples for new binary-only structure * refactor!: made all cli env var config use global arguments instead (see `perseus --help`) * fix(cli): prevented tool download in new/init/clean * refactor: moved env vars for tool version configs into global arguments * refactor: used more object-oriented style for installation code This is more longwinded, but fixes a few priority issues, and it's much more maintainable and extensible. * feat(cli): added system-wide cache for tools This will be used preferentially over a local directory if available, minimizing wait times for creating new projects. * chore(cli): updated `init` example for new binary-only structure * test(cli): planned out tests structure for cli * docs: updated i18n sizes with different translators Our more advanced optimizations cut bundle sizes with Fluent *substantially*. * test: fixed placeholder cli tests * feat(cli): allowed provision of different `cargo`s for browser/engine This will allow properly using the Cranelift backend. BREAKING CHANGE: `[lib]`/`[[bin]]` settings no longer required in `Cargo.toml`, and `lib.rs` should be renamed to `main.rs` (everything is a binary now) --- examples/comprehensive/tiny/Cargo.toml | 10 - .../comprehensive/tiny/Cargo.toml.example | 10 - .../tiny/src/{lib.rs => main.rs} | 0 examples/core/basic/.gitignore | 2 - examples/core/basic/Cargo.toml | 13 - examples/core/basic/Cargo.toml.example | 10 - examples/core/basic/src/{lib.rs => main.rs} | 0 examples/core/custom_server/Cargo.toml | 10 - .../custom_server/src/{lib.rs => main.rs} | 0 examples/core/freezing_and_thawing/Cargo.toml | 10 - .../src/{lib.rs => main.rs} | 0 examples/core/global_state/Cargo.toml | 10 - .../core/global_state/src/{lib.rs => main.rs} | 0 examples/core/i18n/Cargo.toml | 10 - examples/core/i18n/README.md | 2 +- examples/core/i18n/src/{lib.rs => main.rs} | 0 examples/core/idb_freezing/Cargo.toml | 10 - .../core/idb_freezing/src/{lib.rs => main.rs} | 0 examples/core/index_view/Cargo.toml | 10 - .../core/index_view/src/{lib.rs => main.rs} | 0 examples/core/js_interop/.gitignore | 2 - examples/core/js_interop/Cargo.toml | 13 - examples/core/js_interop/Cargo.toml.example | 28 - .../core/js_interop/src/{lib.rs => main.rs} | 0 examples/core/plugins/Cargo.toml | 10 - examples/core/plugins/src/{lib.rs => main.rs} | 0 examples/core/router_state/Cargo.toml | 10 - .../core/router_state/src/{lib.rs => main.rs} | 0 examples/core/rx_state/Cargo.toml | 10 - .../core/rx_state/src/{lib.rs => main.rs} | 0 examples/core/set_headers/Cargo.toml | 10 - .../core/set_headers/src/{lib.rs => main.rs} | 0 examples/core/state_generation/Cargo.toml | 10 - .../state_generation/src/{lib.rs => main.rs} | 0 examples/core/static_content/Cargo.toml | 10 - .../static_content/src/{lib.rs => main.rs} | 0 examples/core/unreactive/Cargo.toml | 10 - .../core/unreactive/src/{lib.rs => main.rs} | 0 examples/demos/auth/Cargo.toml | 10 - examples/demos/auth/src/{lib.rs => main.rs} | 0 examples/demos/fetching/Cargo.toml | 10 - examples/demos/fetching/index.html | 10 - .../demos/fetching/src/{lib.rs => main.rs} | 0 packages/perseus-cli/Cargo.toml | 10 +- packages/perseus-cli/src/bin/main.rs | 114 +++- packages/perseus-cli/src/build.rs | 164 ++--- packages/perseus-cli/src/deploy.rs | 64 +- packages/perseus-cli/src/errors.rs | 108 ++- packages/perseus-cli/src/export.rs | 139 ++-- packages/perseus-cli/src/export_error_page.rs | 20 +- packages/perseus-cli/src/init.rs | 40 +- packages/perseus-cli/src/install.rs | 616 ++++++++++++++++++ packages/perseus-cli/src/lib.rs | 31 +- packages/perseus-cli/src/parse.rs | 92 ++- packages/perseus-cli/src/prepare.rs | 85 ++- packages/perseus-cli/src/reload_server.rs | 24 +- packages/perseus-cli/src/serve.rs | 101 +-- packages/perseus-cli/src/snoop.rs | 68 +- packages/perseus-cli/src/thread.rs | 6 +- packages/perseus-cli/src/tinker.rs | 44 +- packages/perseus-cli/tests/README.md | 45 ++ packages/perseus-cli/tests/new.rs | 19 + packages/perseus-macro/src/entrypoint.rs | 3 - packages/perseus/src/engine/get_op.rs | 1 - packages/perseus/src/engine/serve.rs | 12 + packages/perseus/src/server/html_shell.rs | 9 +- scripts/example.rs | 10 +- 67 files changed, 1464 insertions(+), 611 deletions(-) rename examples/comprehensive/tiny/src/{lib.rs => main.rs} (100%) rename examples/core/basic/src/{lib.rs => main.rs} (100%) rename examples/core/custom_server/src/{lib.rs => main.rs} (100%) rename examples/core/freezing_and_thawing/src/{lib.rs => main.rs} (100%) rename examples/core/global_state/src/{lib.rs => main.rs} (100%) rename examples/core/i18n/src/{lib.rs => main.rs} (100%) rename examples/core/idb_freezing/src/{lib.rs => main.rs} (100%) rename examples/core/index_view/src/{lib.rs => main.rs} (100%) delete mode 100644 examples/core/js_interop/Cargo.toml.example rename examples/core/js_interop/src/{lib.rs => main.rs} (100%) rename examples/core/plugins/src/{lib.rs => main.rs} (100%) rename examples/core/router_state/src/{lib.rs => main.rs} (100%) rename examples/core/rx_state/src/{lib.rs => main.rs} (100%) rename examples/core/set_headers/src/{lib.rs => main.rs} (100%) rename examples/core/state_generation/src/{lib.rs => main.rs} (100%) rename examples/core/static_content/src/{lib.rs => main.rs} (100%) rename examples/core/unreactive/src/{lib.rs => main.rs} (100%) rename examples/demos/auth/src/{lib.rs => main.rs} (100%) delete mode 100644 examples/demos/fetching/index.html rename examples/demos/fetching/src/{lib.rs => main.rs} (100%) create mode 100644 packages/perseus-cli/src/install.rs create mode 100644 packages/perseus-cli/tests/README.md create mode 100644 packages/perseus-cli/tests/new.rs diff --git a/examples/comprehensive/tiny/Cargo.toml b/examples/comprehensive/tiny/Cargo.toml index 109ae6ab6f..cef40b5ae6 100644 --- a/examples/comprehensive/tiny/Cargo.toml +++ b/examples/comprehensive/tiny/Cargo.toml @@ -14,13 +14,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-tiny" -path = "src/lib.rs" diff --git a/examples/comprehensive/tiny/Cargo.toml.example b/examples/comprehensive/tiny/Cargo.toml.example index 24a3f4b72c..bb85cfb2b0 100644 --- a/examples/comprehensive/tiny/Cargo.toml.example +++ b/examples/comprehensive/tiny/Cargo.toml.example @@ -12,13 +12,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-warp = { version = "=0.4.0-beta.4", features = [ "dflt-server" ] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "my-app" -path = "src/lib.rs" diff --git a/examples/comprehensive/tiny/src/lib.rs b/examples/comprehensive/tiny/src/main.rs similarity index 100% rename from examples/comprehensive/tiny/src/lib.rs rename to examples/comprehensive/tiny/src/main.rs diff --git a/examples/core/basic/.gitignore b/examples/core/basic/.gitignore index 6df95ee360..849ddff3b7 100644 --- a/examples/core/basic/.gitignore +++ b/examples/core/basic/.gitignore @@ -1,3 +1 @@ dist/ -target_engine/ -target_wasm/ diff --git a/examples/core/basic/Cargo.toml b/examples/core/basic/Cargo.toml index 71392d2783..45095861a0 100644 --- a/examples/core/basic/Cargo.toml +++ b/examples/core/basic/Cargo.toml @@ -20,16 +20,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-basic" -path = "src/lib.rs" - -[package.metadata.wasm-pack.profile.release] -wasm-opt = [ "-Oz" ] diff --git a/examples/core/basic/Cargo.toml.example b/examples/core/basic/Cargo.toml.example index ac380b99f7..0d8155ad1e 100644 --- a/examples/core/basic/Cargo.toml.example +++ b/examples/core/basic/Cargo.toml.example @@ -16,13 +16,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-warp = { version = "=0.4.0-beta.4", features = [ "dflt-server" ] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "my-app" -path = "src/lib.rs" diff --git a/examples/core/basic/src/lib.rs b/examples/core/basic/src/main.rs similarity index 100% rename from examples/core/basic/src/lib.rs rename to examples/core/basic/src/main.rs diff --git a/examples/core/custom_server/Cargo.toml b/examples/core/custom_server/Cargo.toml index f0c3935169..1036346d50 100644 --- a/examples/core/custom_server/Cargo.toml +++ b/examples/core/custom_server/Cargo.toml @@ -20,13 +20,3 @@ perseus-warp = { path = "../../../packages/perseus-warp", features = [ "dflt-ser warp = { package = "warp-fix-171", version = "0.3" } # Temporary until Warp #171 is resolved [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-custom-server" -path = "src/lib.rs" diff --git a/examples/core/custom_server/src/lib.rs b/examples/core/custom_server/src/main.rs similarity index 100% rename from examples/core/custom_server/src/lib.rs rename to examples/core/custom_server/src/main.rs diff --git a/examples/core/freezing_and_thawing/Cargo.toml b/examples/core/freezing_and_thawing/Cargo.toml index c1f580550c..83184d925d 100644 --- a/examples/core/freezing_and_thawing/Cargo.toml +++ b/examples/core/freezing_and_thawing/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-freezing-and-thawing" -path = "src/lib.rs" diff --git a/examples/core/freezing_and_thawing/src/lib.rs b/examples/core/freezing_and_thawing/src/main.rs similarity index 100% rename from examples/core/freezing_and_thawing/src/lib.rs rename to examples/core/freezing_and_thawing/src/main.rs diff --git a/examples/core/global_state/Cargo.toml b/examples/core/global_state/Cargo.toml index 3825dcf732..e74758ef1f 100644 --- a/examples/core/global_state/Cargo.toml +++ b/examples/core/global_state/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-global-state" -path = "src/lib.rs" diff --git a/examples/core/global_state/src/lib.rs b/examples/core/global_state/src/main.rs similarity index 100% rename from examples/core/global_state/src/lib.rs rename to examples/core/global_state/src/main.rs diff --git a/examples/core/i18n/Cargo.toml b/examples/core/i18n/Cargo.toml index 11277bcb98..6d8559375f 100644 --- a/examples/core/i18n/Cargo.toml +++ b/examples/core/i18n/Cargo.toml @@ -22,13 +22,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-i18n" -path = "src/lib.rs" diff --git a/examples/core/i18n/README.md b/examples/core/i18n/README.md index da4c23affc..66d58f5372 100644 --- a/examples/core/i18n/README.md +++ b/examples/core/i18n/README.md @@ -2,4 +2,4 @@ This example shows a very basic Perseus app using internationalization (abbreviated *i18n*) in three languages: English, French, and Spanish. This shows how to use translations, access them, and how to insert variables into them. -Note that this i18n in this example can use either the [Fluent](https://projectfluent.org) translator or the lightweight translator, so the `translations/` directory has both `.ftl` files (for Fluent), and `.json` files (for the lightweight translator). The optimized produced bundle sizes are 405.3kb and 289.4kb respectively. +Note that this i18n in this example can use either the [Fluent](https://projectfluent.org) translator or the lightweight translator, so the `translations/` directory has both `.ftl` files (for Fluent), and `.json` files (for the lightweight translator). The optimized produced bundle sizes are 336kb and 247.8kb respectively. diff --git a/examples/core/i18n/src/lib.rs b/examples/core/i18n/src/main.rs similarity index 100% rename from examples/core/i18n/src/lib.rs rename to examples/core/i18n/src/main.rs diff --git a/examples/core/idb_freezing/Cargo.toml b/examples/core/idb_freezing/Cargo.toml index 67a0117205..d287f13c8e 100644 --- a/examples/core/idb_freezing/Cargo.toml +++ b/examples/core/idb_freezing/Cargo.toml @@ -20,13 +20,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-idb-freezing" -path = "src/lib.rs" diff --git a/examples/core/idb_freezing/src/lib.rs b/examples/core/idb_freezing/src/main.rs similarity index 100% rename from examples/core/idb_freezing/src/lib.rs rename to examples/core/idb_freezing/src/main.rs diff --git a/examples/core/index_view/Cargo.toml b/examples/core/index_view/Cargo.toml index 21ac026353..5d7bec62a0 100644 --- a/examples/core/index_view/Cargo.toml +++ b/examples/core/index_view/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-index-view" -path = "src/lib.rs" diff --git a/examples/core/index_view/src/lib.rs b/examples/core/index_view/src/main.rs similarity index 100% rename from examples/core/index_view/src/lib.rs rename to examples/core/index_view/src/main.rs diff --git a/examples/core/js_interop/.gitignore b/examples/core/js_interop/.gitignore index 6df95ee360..849ddff3b7 100644 --- a/examples/core/js_interop/.gitignore +++ b/examples/core/js_interop/.gitignore @@ -1,3 +1 @@ dist/ -target_engine/ -target_wasm/ diff --git a/examples/core/js_interop/Cargo.toml b/examples/core/js_interop/Cargo.toml index 632885e919..3c7bbaca33 100644 --- a/examples/core/js_interop/Cargo.toml +++ b/examples/core/js_interop/Cargo.toml @@ -20,16 +20,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-js-interop" -path = "src/lib.rs" - -[package.metadata.wasm-pack.profile.release] -wasm-opt = [ "-Oz" ] diff --git a/examples/core/js_interop/Cargo.toml.example b/examples/core/js_interop/Cargo.toml.example deleted file mode 100644 index ac380b99f7..0000000000 --- a/examples/core/js_interop/Cargo.toml.example +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "my-app" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -perseus = { version = "=0.4.0-beta.4", features = [ "hydrate" ] } -sycamore = "=0.8.0-beta.7" -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } -perseus-warp = { version = "=0.4.0-beta.4", features = [ "dflt-server" ] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "my-app" -path = "src/lib.rs" diff --git a/examples/core/js_interop/src/lib.rs b/examples/core/js_interop/src/main.rs similarity index 100% rename from examples/core/js_interop/src/lib.rs rename to examples/core/js_interop/src/main.rs diff --git a/examples/core/plugins/Cargo.toml b/examples/core/plugins/Cargo.toml index 079f9752c2..92a2312e42 100644 --- a/examples/core/plugins/Cargo.toml +++ b/examples/core/plugins/Cargo.toml @@ -20,13 +20,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-plugins" -path = "src/lib.rs" diff --git a/examples/core/plugins/src/lib.rs b/examples/core/plugins/src/main.rs similarity index 100% rename from examples/core/plugins/src/lib.rs rename to examples/core/plugins/src/main.rs diff --git a/examples/core/router_state/Cargo.toml b/examples/core/router_state/Cargo.toml index 037e46fbe3..5bc9a2a619 100644 --- a/examples/core/router_state/Cargo.toml +++ b/examples/core/router_state/Cargo.toml @@ -16,13 +16,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-router-state" -path = "src/lib.rs" diff --git a/examples/core/router_state/src/lib.rs b/examples/core/router_state/src/main.rs similarity index 100% rename from examples/core/router_state/src/lib.rs rename to examples/core/router_state/src/main.rs diff --git a/examples/core/rx_state/Cargo.toml b/examples/core/rx_state/Cargo.toml index 0e772c05cb..455a687158 100644 --- a/examples/core/rx_state/Cargo.toml +++ b/examples/core/rx_state/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-rx-state" -path = "src/lib.rs" diff --git a/examples/core/rx_state/src/lib.rs b/examples/core/rx_state/src/main.rs similarity index 100% rename from examples/core/rx_state/src/lib.rs rename to examples/core/rx_state/src/main.rs diff --git a/examples/core/set_headers/Cargo.toml b/examples/core/set_headers/Cargo.toml index 0cf8e334c0..c85f787234 100644 --- a/examples/core/set_headers/Cargo.toml +++ b/examples/core/set_headers/Cargo.toml @@ -20,13 +20,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-set-headers" -path = "src/lib.rs" diff --git a/examples/core/set_headers/src/lib.rs b/examples/core/set_headers/src/main.rs similarity index 100% rename from examples/core/set_headers/src/lib.rs rename to examples/core/set_headers/src/main.rs diff --git a/examples/core/state_generation/Cargo.toml b/examples/core/state_generation/Cargo.toml index 09c53a35df..01d1ff21e7 100644 --- a/examples/core/state_generation/Cargo.toml +++ b/examples/core/state_generation/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-state-generation" -path = "src/lib.rs" diff --git a/examples/core/state_generation/src/lib.rs b/examples/core/state_generation/src/main.rs similarity index 100% rename from examples/core/state_generation/src/lib.rs rename to examples/core/state_generation/src/main.rs diff --git a/examples/core/static_content/Cargo.toml b/examples/core/static_content/Cargo.toml index 79c6fec9ff..434dbbdd7d 100644 --- a/examples/core/static_content/Cargo.toml +++ b/examples/core/static_content/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-static-content" -path = "src/lib.rs" diff --git a/examples/core/static_content/src/lib.rs b/examples/core/static_content/src/main.rs similarity index 100% rename from examples/core/static_content/src/lib.rs rename to examples/core/static_content/src/main.rs diff --git a/examples/core/unreactive/Cargo.toml b/examples/core/unreactive/Cargo.toml index 1c9c162cc5..46dda33ea8 100644 --- a/examples/core/unreactive/Cargo.toml +++ b/examples/core/unreactive/Cargo.toml @@ -19,13 +19,3 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-unreactive" -path = "src/lib.rs" diff --git a/examples/core/unreactive/src/lib.rs b/examples/core/unreactive/src/main.rs similarity index 100% rename from examples/core/unreactive/src/lib.rs rename to examples/core/unreactive/src/main.rs diff --git a/examples/demos/auth/Cargo.toml b/examples/demos/auth/Cargo.toml index ef3f91c6a2..d558aad3a5 100644 --- a/examples/demos/auth/Cargo.toml +++ b/examples/demos/auth/Cargo.toml @@ -17,15 +17,5 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" # We need the `HtmlDocument` feature to be able to use cookies (which this example does) web-sys = { version = "0.3", features = [ "Storage" ] } - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-auth" -path = "src/lib.rs" diff --git a/examples/demos/auth/src/lib.rs b/examples/demos/auth/src/main.rs similarity index 100% rename from examples/demos/auth/src/lib.rs rename to examples/demos/auth/src/main.rs diff --git a/examples/demos/fetching/Cargo.toml b/examples/demos/fetching/Cargo.toml index 8da8154c5c..477f896cca 100644 --- a/examples/demos/fetching/Cargo.toml +++ b/examples/demos/fetching/Cargo.toml @@ -17,14 +17,4 @@ perseus-integration = { path = "../../../packages/perseus-integration", default- reqwest = "0.11" [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" reqwasm = "0.4" - -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "perseus-example-fetching" -path = "src/lib.rs" diff --git a/examples/demos/fetching/index.html b/examples/demos/fetching/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/demos/fetching/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/demos/fetching/src/lib.rs b/examples/demos/fetching/src/main.rs similarity index 100% rename from examples/demos/fetching/src/lib.rs rename to examples/demos/fetching/src/main.rs diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index 1bca74027f..821a388c3a 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -37,7 +37,15 @@ ctrlc = { version = "3.0", features = ["termination"] } notify = "=5.0.0-pre.13" futures = "0.3" tokio-stream = "0.1" -ureq = "2" +reqwest = { version = "0.11", features = [ "json", "stream" ] } +tar = "0.4" +flate2 = "1" +home = "0.5" + +[dev-dependencies] +assert_cmd = "2" +assert_fs = "1" +predicates = "2" [lib] name = "perseus_cli" diff --git a/packages/perseus-cli/src/bin/main.rs b/packages/perseus-cli/src/bin/main.rs index 3907a4d0c7..8919c84487 100644 --- a/packages/perseus-cli/src/bin/main.rs +++ b/packages/perseus-cli/src/bin/main.rs @@ -1,6 +1,7 @@ use clap::Parser; use command_group::stdlib::CommandGroup; use fmterr::fmt_err; +use home::home_dir; use notify::{recommended_watcher, RecursiveMode, Watcher}; use perseus_cli::parse::{ExportOpts, ServeOpts, SnoopSubcommand}; use perseus_cli::{ @@ -9,11 +10,10 @@ use perseus_cli::{ serve, serve_exported, tinker, }; use perseus_cli::{ - delete_dist, errors::*, export_error_page, order_reload, run_reload_server, snoop_build, - snoop_server, snoop_wasm_build, + create_dist, delete_dist, errors::*, export_error_page, order_reload, run_reload_server, + snoop_build, snoop_server, snoop_wasm_build, Tools, }; use std::env; -use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::mpsc::channel; @@ -48,6 +48,26 @@ async fn real_main() -> i32 { Ok(exit_code) => exit_code, // If something failed, we print the error to `stderr` and return a failure exit code Err(err) => { + // Check if this was an error with tool installation (in which case we should + // delete the tools directory to *try* to avoid corruptions) + if matches!(err, Error::InstallError(_)) { + // We'll try to delete *both* the local one and the system-wide cache + if let Err(err) = delete_artifacts(dir.clone(), "tools") { + eprintln!("{}", fmt_err(&err)); + } + if let Some(path) = home_dir() { + let target = path.join(".cargo/perseus_tools"); + if target.exists() { + if let Err(err) = std::fs::remove_dir_all(&target) { + let err = ExecutionError::RemoveArtifactsFailed { + target: target.to_str().map(|s| s.to_string()), + source: err, + }; + eprintln!("{}", fmt_err(&err)) + } + } + } + } eprintln!("{}", fmt_err(&err)); 1 } @@ -58,7 +78,7 @@ async fn real_main() -> i32 { enum Event { // Sent if we should restart the child process Reload, - // Sent if we should temrinate the child process + // Sent if we should terminate the child process Terminate, } @@ -69,20 +89,18 @@ enum Event { // vector in testing If at any point a warning can't be printed, the program // will panic async fn core(dir: PathBuf) -> Result { - // Get `stdout` so we can write warnings appropriately - let stdout = &mut std::io::stdout(); + // Parse the CLI options with `clap` + let opts = Opts::parse(); // Warn the user if they're using the CLI single-threaded mode - if env::var("PERSEUS_CLI_SEQUENTIAL").is_ok() { - writeln!(stdout, "Note: the Perseus CLI is running in single-threaded mode, which is less performant on most modern systems. You can switch to multi-threaded mode by unsetting the 'PERSEUS_CLI_SEQUENTIAL' environment variable. If you've deliberately enabled single-threaded mode, you can safely ignore this.\n").expect("Failed to write to stdout."); + if opts.sequential { + println!("Note: the Perseus CLI is running in single-threaded mode, which is less performant on most modern systems. You can switch to multi-threaded mode by unsetting the 'PERSEUS_CLI_SEQUENTIAL' environment variable. If you've deliberately enabled single-threaded mode, you can safely ignore this."); } - // Parse the CLI options with `clap` - let opts = Opts::parse(); // Check the user's environment to make sure they have prerequisites // We do this after any help pages or version numbers have been parsed for // snappiness - check_env()?; + check_env(&opts)?; // Check if this process is allowed to watch for changes // This will be set to `true` if this is a child process @@ -118,9 +136,14 @@ async fn core(dir: PathBuf) -> Result { // Set up a browser reloading server // We provide an option for the user to disable this - if env::var("PERSEUS_NO_BROWSER_RELOAD").is_err() { + let Opts { + reload_server_host, + reload_server_port, + .. + } = opts.clone(); + if !opts.no_browser_reload { tokio::task::spawn(async move { - run_reload_server().await; + run_reload_server(reload_server_host, reload_server_port).await; }); } @@ -234,72 +257,93 @@ async fn core(dir: PathBuf) -> Result { } async fn core_watch(dir: PathBuf, opts: Opts) -> Result { + create_dist(&dir)?; + + // We install the tools for every command except `new`, `init`, and `clean` let exit_code = match opts.subcmd { - Subcommand::Build(build_opts) => { + Subcommand::Build(ref build_opts) => { + let tools = Tools::new(&dir, &opts).await?; // Delete old build artifacts delete_artifacts(dir.clone(), "static")?; - build(dir, build_opts)? + build(dir, build_opts, &tools, &opts)? } - Subcommand::Export(export_opts) => { + Subcommand::Export(ref export_opts) => { + let tools = Tools::new(&dir, &opts).await?; // Delete old build/export artifacts delete_artifacts(dir.clone(), "static")?; delete_artifacts(dir.clone(), "exported")?; - let exit_code = export(dir.clone(), export_opts.clone())?; + let exit_code = export(dir.clone(), export_opts, &tools, &opts)?; if exit_code != 0 { return Ok(exit_code); } if export_opts.serve { // Tell any connected browsers to reload - order_reload(); - serve_exported(dir, export_opts.host, export_opts.port).await; + order_reload(opts.reload_server_host.to_string(), opts.reload_server_port); + serve_exported(dir, export_opts.host.to_string(), export_opts.port).await; } 0 } - Subcommand::Serve(serve_opts) => { + Subcommand::Serve(ref serve_opts) => { + let tools = Tools::new(&dir, &opts).await?; if !serve_opts.no_build { delete_artifacts(dir.clone(), "static")?; } // This orders reloads internally - let (exit_code, _server_path) = serve(dir, serve_opts)?; + let (exit_code, _server_path) = serve(dir, serve_opts, &tools, &opts)?; exit_code } - Subcommand::Test(test_opts) => { + Subcommand::Test(ref test_opts) => { + let tools = Tools::new(&dir, &opts).await?; // This will be used by the subcrates env::set_var("PERSEUS_TESTING", "true"); // Delete old build artifacts if `--no-build` wasn't specified if !test_opts.no_build { delete_artifacts(dir.clone(), "static")?; } - let (exit_code, _server_path) = serve(dir, test_opts)?; + let (exit_code, _server_path) = serve(dir, test_opts, &tools, &opts)?; exit_code } Subcommand::Clean => { delete_dist(dir)?; + // Warn the user that the next run will be quite a bit slower + eprintln!( + "[NOTE]: Build artifacts have been deleted, the next run will take some time." + ); 0 } - Subcommand::Deploy(deploy_opts) => { + Subcommand::Deploy(ref deploy_opts) => { + let tools = Tools::new(&dir, &opts).await?; delete_artifacts(dir.clone(), "static")?; delete_artifacts(dir.clone(), "exported")?; delete_artifacts(dir.clone(), "pkg")?; - deploy(dir, deploy_opts)? + deploy(dir, deploy_opts, &tools, &opts)? } - Subcommand::Tinker(tinker_opts) => { + Subcommand::Tinker(ref tinker_opts) => { + let tools = Tools::new(&dir, &opts).await?; // Unless we've been told not to, we start with a blank slate // This will remove old tinkerings and eliminate any possible corruptions (which // are very likely with tinkering!) if !tinker_opts.no_clean { delete_dist(dir.clone())?; } - tinker(dir)? + tinker(dir, &tools, &opts)? + } + Subcommand::Snoop(ref snoop_subcmd) => { + let tools = Tools::new(&dir, &opts).await?; + match snoop_subcmd { + SnoopSubcommand::Build => snoop_build(dir, &tools, &opts)?, + SnoopSubcommand::WasmBuild => snoop_wasm_build(dir, &tools, &opts)?, + SnoopSubcommand::Serve(ref snoop_serve_opts) => { + snoop_server(dir, snoop_serve_opts, &tools, &opts)? + } + } + } + Subcommand::ExportErrorPage(ref eep_opts) => { + let tools = Tools::new(&dir, &opts).await?; + export_error_page(dir, eep_opts, &tools, &opts)? } - Subcommand::Snoop(snoop_subcmd) => match snoop_subcmd { - SnoopSubcommand::Build => snoop_build(dir)?, - SnoopSubcommand::WasmBuild(snoop_wasm_opts) => snoop_wasm_build(dir, snoop_wasm_opts)?, - SnoopSubcommand::Serve(snoop_serve_opts) => snoop_server(dir, snoop_serve_opts)?, - }, - Subcommand::ExportErrorPage(opts) => export_error_page(dir, opts)?, - Subcommand::New(opts) => new(dir, opts)?, - Subcommand::Init(opts) => init(dir, opts)?, + Subcommand::New(ref new_opts) => new(dir, new_opts, &opts)?, + Subcommand::Init(ref init_opts) => init(dir, init_opts)?, }; Ok(exit_code) } diff --git a/packages/perseus-cli/src/build.rs b/packages/perseus-cli/src/build.rs index f94fde1ce2..b676392966 100644 --- a/packages/perseus-cli/src/build.rs +++ b/packages/perseus-cli/src/build.rs @@ -1,10 +1,10 @@ use crate::cmd::{cfg_spinner, run_stage}; -use crate::errors::*; -use crate::parse::BuildOpts; +use crate::install::Tools; +use crate::parse::{BuildOpts, Opts}; use crate::thread::{spawn_thread, ThreadHandle}; +use crate::{errors::*, get_user_crate_name}; use console::{style, Emoji}; use indicatif::{MultiProgress, ProgressBar}; -use std::env; use std::path::PathBuf; // Emojis for stages @@ -21,23 +21,6 @@ macro_rules! handle_exit_code { }; } -// /// Finalizes the build by renaming some directories. -// pub fn finalize(target: &Path) -> Result<(), ExecutionError> { -// // Move the `pkg/` directory into `dist/pkg/` -// let pkg_dir = target.join("dist/pkg"); -// if pkg_dir.exists() { -// if let Err(err) = fs::remove_dir_all(&pkg_dir) { -// return Err(ExecutionError::MovePkgDirFailed { source: err }); -// } -// } -// // The `fs::rename()` function will fail on Windows if the destination already exists, so this should work (we've just deleted it as per https://github.com/rust-lang/rust/issues/31301#issuecomment-177117325) -// if let Err(err) = fs::rename(target.join("pkg"), target.join("dist/pkg")) -// { return Err(ExecutionError::MovePkgDirFailed { source: err }); -// } - -// Ok(()) -// } - /// Actually builds the user's code, program arguments having been interpreted. /// This needs to know how many steps there are in total because the serving /// logic also uses it. This also takes a `MultiProgress` to interact with so it @@ -49,6 +32,8 @@ pub fn build_internal( spinners: &MultiProgress, num_steps: u8, is_release: bool, + tools: &Tools, + global_opts: &Opts, ) -> Result< ( ThreadHandle Result, Result>, @@ -56,6 +41,18 @@ pub fn build_internal( ), ExecutionError, > { + // We need to own this for the threads + let tools = tools.clone(); + let Opts { + wasm_release_rustflags, + cargo_engine_args, + cargo_browser_args, + wasm_bindgen_args, + wasm_opt_args, + .. + } = global_opts.clone(); + + let crate_name = get_user_crate_name(&dir)?; // Static generation message let sg_msg = format!( "{} {} Generating your app", @@ -69,14 +66,6 @@ pub fn build_internal( BUILDING ); - // Prepare the optimization flags for the Wasm build (only used in release mode) - let wasm_opt_flags = if is_release { - env::var("PERSEUS_WASM_RELEASE_RUSTFLAGS") - .unwrap_or_else(|_| "-C opt-level=z -C codegen-units=1".to_string()) - } else { - String::new() - }; - // We parallelize the first two spinners (static generation and Wasm building) // We make sure to add them at the top (the server spinner may have already been // instantiated) @@ -86,58 +75,92 @@ pub fn build_internal( let wb_spinner = spinners.insert(1, ProgressBar::new_spinner()); let wb_spinner = cfg_spinner(wb_spinner, &wb_msg); let wb_dir = dir; - let sg_thread = spawn_thread(move || { - handle_exit_code!(run_stage( - vec![&format!( - "{} run {} {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - if is_release { "--release" } else { "" }, - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) - )], - &sg_dir, - &sg_spinner, - &sg_msg, - vec![ - ("PERSEUS_ENGINE_OPERATION", "build"), - ("CARGO_TARGET_DIR", "target_engine") - ] - )?); - - Ok(0) - }); - let wb_thread = spawn_thread(move || { - handle_exit_code!(run_stage( - vec![&format!( - "{} build --out-dir dist/pkg --out-name perseus_engine --target web {} {}", - env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - if is_release { "--release" } else { "--dev" }, /* If we don't supply `--dev`, another profile will be used */ - env::var("PERSEUS_WASM_PACK_ARGS").unwrap_or_else(|_| String::new()) - )], - &wb_dir, - &wb_spinner, - &wb_msg, - if is_release { + let cargo_engine_exec = tools.cargo_engine.clone(); + let sg_thread = spawn_thread( + move || { + handle_exit_code!(run_stage( + vec![&format!( + "{} run {} {}", + cargo_engine_exec, + if is_release { "--release" } else { "" }, + cargo_engine_args + )], + &sg_dir, + &sg_spinner, + &sg_msg, vec![ - ("CARGO_TARGET_DIR", "target_wasm"), - ("RUSTFLAGS", &wasm_opt_flags), + ("PERSEUS_ENGINE_OPERATION", "build"), + ("CARGO_TARGET_DIR", "dist/target_engine") ] - } else { - vec![("CARGO_TARGET_DIR", "target_wasm")] + )?); + + Ok(0) + }, + global_opts.sequential, + ); + let wb_thread = spawn_thread( + move || { + let mut cmds = vec![ + // Build the Wasm artifact first (and we know where it will end up, since we're setting the target directory) + format!( + "{} build --target wasm32-unknown-unknown {} {}", + tools.cargo_browser, + if is_release { "--release" } else { "" }, + cargo_browser_args + ), + // NOTE The `wasm-bindgen` version has to be *identical* to the dependency version + format!( + "{cmd} ./dist/target_wasm/wasm32-unknown-unknown/{profile}/{crate_name}.wasm --out-dir dist/pkg --out-name perseus_engine --target web {args}", + cmd=tools.wasm_bindgen, + profile={ if is_release { "release" } else { "debug" } }, + args=wasm_bindgen_args, + crate_name=crate_name + ) + ]; + // If we're building for release, then we should run `wasm-opt` + if is_release { + cmds.push(format!( + "{cmd} -Oz ./dist/pkg/perseus_engine_bg.wasm -o ./dist/pkg/perseus_engine_bg.wasm {args}", + cmd=tools.wasm_opt, + args=wasm_opt_args + )); } - )?); + let cmds = cmds.iter().map(|s| s.as_str()).collect::>(); + handle_exit_code!(run_stage( + cmds, + &wb_dir, + &wb_spinner, + &wb_msg, + if is_release { + vec![ + ("CARGO_TARGET_DIR", "dist/target_wasm"), + ("RUSTFLAGS", &wasm_release_rustflags), + ] + } else { + vec![("CARGO_TARGET_DIR", "dist/target_wasm")] + } + )?); - Ok(0) - }); + Ok(0) + }, + global_opts.sequential, + ); Ok((sg_thread, wb_thread)) } /// Builds the subcrates to get a directory that we can serve. Returns an exit /// code. -pub fn build(dir: PathBuf, opts: BuildOpts) -> Result { +pub fn build( + dir: PathBuf, + opts: &BuildOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result { let spinners = MultiProgress::new(); - let (sg_thread, wb_thread) = build_internal(dir, &spinners, 2, opts.release)?; + let (sg_thread, wb_thread) = + build_internal(dir, &spinners, 2, opts.release, tools, global_opts)?; let sg_res = sg_thread .join() .map_err(|_| ExecutionError::ThreadWaitFailed)??; @@ -151,11 +174,6 @@ pub fn build(dir: PathBuf, opts: BuildOpts) -> Result { return Ok(wb_res); } - // This waits for all the threads and lets the spinners draw to the terminal - // spinners.join().map_err(|_| ErrorKind::ThreadWaitFailed)?; - // And now we can run the finalization stage - // finalize(&dir)?; - // We've handled errors in the component threads, so the exit code is now zero Ok(0) } diff --git a/packages/perseus-cli/src/deploy.rs b/packages/perseus-cli/src/deploy.rs index 38369aad0a..a6483612f7 100644 --- a/packages/perseus-cli/src/deploy.rs +++ b/packages/perseus-cli/src/deploy.rs @@ -1,5 +1,7 @@ use crate::errors::*; use crate::export; +use crate::install::Tools; +use crate::parse::Opts; use crate::parse::{DeployOpts, ExportOpts, ServeOpts}; use crate::serve; use fs_extra::copy_items; @@ -12,12 +14,17 @@ use std::path::PathBuf; /// together in one folder that can be conveniently uploaded to a server, file /// host, etc. This can return any kind of error because deploying involves /// working with other subcommands. -pub fn deploy(dir: PathBuf, opts: DeployOpts) -> Result { +pub fn deploy( + dir: PathBuf, + opts: &DeployOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result { // Fork at whether we're using static exporting or not let exit_code = if opts.export_static { - deploy_export(dir, opts.output)? + deploy_export(dir, opts.output.to_string(), tools, global_opts)? } else { - deploy_full(dir, opts.output)? + deploy_full(dir, opts.output.to_string(), tools, global_opts)? }; Ok(exit_code) @@ -26,11 +33,16 @@ pub fn deploy(dir: PathBuf, opts: DeployOpts) -> Result { /// Deploys the user's app in its entirety, with a bundled server. This can /// return any kind of error because deploying involves working with other /// subcommands. -fn deploy_full(dir: PathBuf, output: String) -> Result { +fn deploy_full( + dir: PathBuf, + output: String, + tools: &Tools, + global_opts: &Opts, +) -> Result { // Build everything for production, not running the server let (serve_exit_code, server_path) = serve( dir.clone(), - ServeOpts { + &ServeOpts { no_run: true, no_build: false, release: true, @@ -42,6 +54,8 @@ fn deploy_full(dir: PathBuf, output: String) -> Result { host: "127.0.0.1".to_string(), port: 8080, }, + tools, + global_opts, )?; if serve_exit_code != 0 { return Ok(serve_exit_code); @@ -103,9 +117,23 @@ fn deploy_full(dir: PathBuf, output: String) -> Result { .into()); } } - // Copy in the entire `dist` directory (it must exist) - let from = dir.join("dist"); - if let Err(err) = copy_dir(&from, &output, &CopyOptions::new()) { + // Create the `dist/` directory in the output directory + if let Err(err) = fs::create_dir(&output_path.join("dist")) { + return Err(DeployError::CreateDistDirFailed { source: err }.into()); + } + // Copy in the different parts of the `dist/` directory that we need (they all + // have to exist) + let from = dir.join("dist/static"); + if let Err(err) = copy_dir(&from, &output_path.join("dist"), &CopyOptions::new()) { + return Err(DeployError::MoveDirFailed { + to: output, + from: from.to_str().map(|s| s.to_string()).unwrap(), + source: err, + } + .into()); + } + let from = dir.join("dist/pkg"); + if let Err(err) = copy_dir(&from, &output_path.join("dist"), &CopyOptions::new()) { return Err(DeployError::MoveDirFailed { to: output, from: from.to_str().map(|s| s.to_string()).unwrap(), @@ -113,6 +141,15 @@ fn deploy_full(dir: PathBuf, output: String) -> Result { } .into()); } + let from = dir.join("dist/render_conf.json"); + if let Err(err) = fs::copy(&from, &output_path.join("dist/render_conf.json")) { + return Err(DeployError::MoveAssetFailed { + to: output, + from: from.to_str().map(|s| s.to_string()).unwrap(), + source: err, + } + .into()); + } println!(); println!("Deployment complete 🚀! Your app is now available for serving in the standalone folder '{}'! You can run it by executing the `server` binary in that folder.", &output_path.to_str().map(|s| s.to_string()).unwrap()); @@ -126,11 +163,16 @@ fn deploy_full(dir: PathBuf, output: String) -> Result { /// Uses static exporting to deploy the user's app. This can return any kind of /// error because deploying involves working with other subcommands. -fn deploy_export(dir: PathBuf, output: String) -> Result { +fn deploy_export( + dir: PathBuf, + output: String, + tools: &Tools, + global_opts: &Opts, +) -> Result { // Export the app to `.perseus/exported`, using release mode let export_exit_code = export( dir.clone(), - ExportOpts { + &ExportOpts { release: true, serve: false, host: String::new(), @@ -138,6 +180,8 @@ fn deploy_export(dir: PathBuf, output: String) -> Result { watch: false, custom_watch: Vec::new(), }, + tools, + global_opts, )?; if export_exit_code != 0 { return Ok(export_exit_code); diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index 9e86478108..25eefb5d77 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -5,13 +5,15 @@ use thiserror::Error; /// All errors that can be returned by the CLI. #[derive(Error, Debug)] pub enum Error { - #[error("prerequisite command execution failed for prerequisite '{cmd}' (set '{env_var}' to another location if you've installed it elsewhere)")] - PrereqNotPresent { - cmd: String, - env_var: String, + #[error("couldn't find `cargo`, which is a dependency of this cli (set 'PERSEUS_CARGO_PATH' to another location if you've installed it elsewhere)")] + CargoNotPresent { #[source] source: std::io::Error, }, + #[error( + "couldn't install `wasm32-unknown-unknown` target (do you have an internet connection?)" + )] + RustupTargetAddFailed { code: i32 }, #[error("couldn't get current directory (have you just deleted it?)")] CurrentDirUnavailable { #[source] @@ -29,6 +31,8 @@ pub enum Error { InitError(#[from] InitError), #[error(transparent)] NewError(#[from] NewError), + #[error(transparent)] + InstallError(#[from] InstallError), } /// Errors that can occur while attempting to execute a Perseus app with @@ -65,6 +69,18 @@ pub enum ExecutionError { #[source] source: std::num::ParseIntError, }, + #[error("couldn't parse `Cargo.toml` (are you running in the right directory?)")] + GetManifestFailed { + #[source] + source: cargo_toml::Error, + }, + #[error("couldn't get crate name from `[package]` section of `Cargo.toml` (are you running in the right directory?)")] + CrateNameNotPresentInManifest, + #[error("couldn't create directory for distribution artifacts (do you have the necessary permissions?)")] + CreateDistFailed { + #[source] + source: std::io::Error, + }, } /// Errors that can occur while running `perseus export`. @@ -125,6 +141,11 @@ pub enum DeployError { #[source] source: std::io::Error, }, + #[error("couldn't create distribution artifacts directory for deployment (if this persists, try `perseus clean`)")] + CreateDistDirFailed { + #[source] + source: std::io::Error, + }, } #[derive(Error, Debug)] @@ -210,3 +231,82 @@ pub enum NewError { source: std::io::Error, }, } + +#[derive(Error, Debug)] +pub enum InstallError { + #[error("couldn't create `dist/tools/` for external dependency installation")] + CreateToolsDirFailed { + #[source] + source: std::io::Error, + }, + // This will only be called after we've checked if the user has already installed the tool + // themselves + #[error("couldn't install '{tool}', as there are no precompiled binaries for your platform and it's not currently installed; please install this tool manually (see https://arctic-hen7.github.io/perseus/en-US/docs/0.4.x/reference/faq)")] + ExternalToolUnavailable { + tool: String, + // This is from checking if the tool is installed at the usual path + #[source] + source: std::io::Error, + }, + #[error("couldn't download binary for '{tool}' (do you have an internet connection?)")] + BinaryDownloadRequestFailed { + tool: String, + #[source] + source: reqwest::Error, + }, + #[error( + "couldn't create destination for tool download (do you have the necessary permissions?)" + )] + CreateToolDownloadDestFailed { + #[source] + source: tokio::io::Error, + }, + #[error("couldn't chunk tool download properly (do you have an internet connection?)")] + ChunkBinaryDownloadFailed { + #[source] + source: reqwest::Error, + }, + #[error( + "couldn't write downloaded chunk of external tool (do you have the necessary permissions?)" + )] + WriteBinaryDownloadChunkFailed { + #[source] + source: tokio::io::Error, + }, + #[error("couldn't determine latest version of '{tool}' (do you have an internet connection?)")] + GetLatestToolVersionFailed { + tool: String, + #[source] + source: reqwest::Error, + }, + #[error("couldn't parse latest version of '{tool}' (if this error persists, please report it as a bug)")] + ParseToolVersionFailed { tool: String }, + #[error("couldn't create destination for extraction of external tool (do you have the necessary permissions?)")] + CreateToolExtractDestFailed { + #[source] + source: std::io::Error, + }, + #[error("couldn't extract '{tool}' (do you have the necessary permissions?)")] + ToolExtractFailed { + tool: String, + #[source] + source: std::io::Error, + }, + #[error("couldn't delete archive from tool deletion")] + ArchiveDeletionFailed { + #[source] + source: std::io::Error, + }, + #[error("couldn't rename directory for external tool binaries")] + DirRenameFailed { + #[source] + source: std::io::Error, + }, + #[error("couldn't read `dist/tools/` to determine which tool versions were installed (do you have the necessary permissions?)")] + ReadToolsDirFailed { + #[source] + source: std::io::Error, + }, + #[error("directory found in `dist/tools/` with invalid name (running `perseus clean` should resolve this)")] + InvalidToolsDirName { name: String }, +} diff --git a/packages/perseus-cli/src/export.rs b/packages/perseus-cli/src/export.rs index 123db928fe..1b4d736c19 100644 --- a/packages/perseus-cli/src/export.rs +++ b/packages/perseus-cli/src/export.rs @@ -1,10 +1,10 @@ use crate::cmd::{cfg_spinner, run_stage}; -use crate::errors::*; -use crate::parse::ExportOpts; +use crate::install::Tools; +use crate::parse::{ExportOpts, Opts}; use crate::thread::{spawn_thread, ThreadHandle}; +use crate::{errors::*, get_user_crate_name}; use console::{style, Emoji}; use indicatif::{MultiProgress, ProgressBar}; -use std::env; use std::fs; use std::path::{Path, PathBuf}; @@ -128,6 +128,8 @@ pub fn export_internal( spinners: &MultiProgress, num_steps: u8, is_release: bool, + tools: &Tools, + global_opts: &Opts, ) -> Result< ( ThreadHandle Result, Result>, @@ -135,6 +137,17 @@ pub fn export_internal( ), ExportError, > { + let tools = tools.clone(); + let Opts { + cargo_browser_args, + cargo_engine_args, + wasm_bindgen_args, + wasm_opt_args, + wasm_release_rustflags, + .. + } = global_opts.clone(); + let crate_name = get_user_crate_name(&dir)?; + // Exporting pages message let ep_msg = format!( "{} {} Exporting your app's pages", @@ -157,54 +170,100 @@ pub fn export_internal( let wb_spinner = spinners.insert(1, ProgressBar::new_spinner()); let wb_spinner = cfg_spinner(wb_spinner, &wb_msg); let wb_target = dir; - let ep_thread = spawn_thread(move || { - handle_exit_code!(run_stage( - vec![&format!( - "{} run {} {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), + let cargo_engine_exec = tools.cargo_engine.clone(); + let ep_thread = spawn_thread( + move || { + handle_exit_code!(run_stage( + vec![&format!( + "{} run {} {}", + cargo_engine_exec, + if is_release { "--release" } else { "" }, + cargo_engine_args + )], + &ep_target, + &ep_spinner, + &ep_msg, + vec![ + ("PERSEUS_ENGINE_OPERATION", "export"), + ("CARGO_TARGET_DIR", "dist/target_engine") + ] + )?); + + Ok(0) + }, + global_opts.sequential, + ); + let wb_thread = spawn_thread( + move || { + let mut cmds = vec![ + // Build the Wasm artifact first (and we know where it will end up, since we're setting the target directory) + format!( + "{} build --target wasm32-unknown-unknown {} {}", + tools.cargo_browser, if is_release { "--release" } else { "" }, - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) - )], - &ep_target, - &ep_spinner, - &ep_msg, - vec![ - ("PERSEUS_ENGINE_OPERATION", "export"), - ("CARGO_TARGET_DIR", "target_engine") - ] - )?); - - Ok(0) - }); - let wb_thread = spawn_thread(move || { - handle_exit_code!(run_stage( - vec![&format!( - "{} build --out-dir dist/pkg --out-name perseus_engine --target web {} {}", - env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - if is_release { "--release" } else { "--dev" }, - env::var("PERSEUS_WASM_PACK_ARGS").unwrap_or_else(|_| String::new()) - )], - &wb_target, - &wb_spinner, - &wb_msg, - vec![("CARGO_TARGET_DIR", "target_wasm")] - )?); - - Ok(0) - }); + cargo_browser_args + ), + // NOTE The `wasm-bindgen` version has to be *identical* to the dependency version + format!( + "{cmd} ./dist/target_wasm/wasm32-unknown-unknown/{profile}/{crate_name}.wasm --out-dir dist/pkg --out-name perseus_engine --target web {args}", + cmd=tools.wasm_bindgen, + profile={ if is_release { "release" } else { "debug" } }, + args=wasm_bindgen_args, + crate_name=crate_name + ) + ]; + // If we're building for release, then we should run `wasm-opt` + if is_release { + cmds.push(format!( + "{cmd} -Oz ./dist/pkg/perseus_engine_bg.wasm -o ./dist/pkg/perseus_engine_bg.wasm {args}", + cmd=tools.wasm_opt, + args=wasm_opt_args + )); + } + let cmds = cmds.iter().map(|s| s.as_str()).collect::>(); + handle_exit_code!(run_stage( + cmds, + &wb_target, + &wb_spinner, + &wb_msg, + if is_release { + vec![ + ("CARGO_TARGET_DIR", "dist/target_wasm"), + ("RUSTFLAGS", &wasm_release_rustflags), + ] + } else { + vec![("CARGO_TARGET_DIR", "dist/target_wasm")] + } + )?); + + Ok(0) + }, + global_opts.sequential, + ); Ok((ep_thread, wb_thread)) } /// Builds the subcrates to get a directory that we can serve. Returns an exit /// code. -pub fn export(dir: PathBuf, opts: ExportOpts) -> Result { +pub fn export( + dir: PathBuf, + opts: &ExportOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result { let spinners = MultiProgress::new(); // We'll add another not-quite-spinner if we're serving let num_spinners = if opts.serve { 3 } else { 2 }; - let (ep_thread, wb_thread) = - export_internal(dir.clone(), &spinners, num_spinners, opts.release)?; + let (ep_thread, wb_thread) = export_internal( + dir.clone(), + &spinners, + num_spinners, + opts.release, + tools, + global_opts, + )?; let ep_res = ep_thread .join() .map_err(|_| ExecutionError::ThreadWaitFailed)??; diff --git a/packages/perseus-cli/src/export_error_page.rs b/packages/perseus-cli/src/export_error_page.rs index b734e0af81..47e4db37f1 100644 --- a/packages/perseus-cli/src/export_error_page.rs +++ b/packages/perseus-cli/src/export_error_page.rs @@ -1,22 +1,30 @@ use crate::cmd::run_cmd_directly; use crate::errors::ExecutionError; -use crate::parse::ExportErrorPageOpts; -use std::env; +use crate::install::Tools; +use crate::parse::{ExportErrorPageOpts, Opts}; use std::path::PathBuf; /// Exports a single error page for the given HTTP status code to the given /// location. -pub fn export_error_page(dir: PathBuf, opts: ExportErrorPageOpts) -> Result { +pub fn export_error_page( + dir: PathBuf, + opts: &ExportErrorPageOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result { run_cmd_directly( format!( "{} run {} -- {} {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()), + tools.cargo_engine, + global_opts.cargo_engine_args, // These are mandatory opts.code, opts.output, ), &dir, - vec![("PERSEUS_ENGINE_OPERATION", "export_error_page")], + vec![ + ("PERSEUS_ENGINE_OPERATION", "export_error_page"), + ("CARGO_TARGET_DIR", "dist/target_engine"), + ], ) } diff --git a/packages/perseus-cli/src/init.rs b/packages/perseus-cli/src/init.rs index 07355d8625..004054e551 100644 --- a/packages/perseus-cli/src/init.rs +++ b/packages/perseus-cli/src/init.rs @@ -1,6 +1,6 @@ use crate::cmd::run_cmd_directly; use crate::errors::*; -use crate::parse::{InitOpts, NewOpts}; +use crate::parse::{InitOpts, NewOpts, Opts}; use std::fs; use std::path::{Path, PathBuf}; @@ -26,7 +26,7 @@ fn create_file_if_not_present( /// Initializes a new Perseus project in the given directory, based on either /// the default template or one from a given URL. -pub fn init(dir: PathBuf, opts: InitOpts) -> Result { +pub fn init(dir: PathBuf, opts: &InitOpts) -> Result { // Create the basic directory structure (this will create both `src/` and // `src/templates/`) fs::create_dir_all(dir.join("src/templates")) @@ -34,7 +34,7 @@ pub fn init(dir: PathBuf, opts: InitOpts) -> Result { // Now create each file create_file_if_not_present(&dir.join("Cargo.toml"), DFLT_INIT_CARGO_TOML, &opts.name)?; create_file_if_not_present(&dir.join(".gitignore"), DFLT_INIT_GITIGNORE, &opts.name)?; - create_file_if_not_present(&dir.join("src/lib.rs"), DFLT_INIT_LIB_RS, &opts.name)?; + create_file_if_not_present(&dir.join("src/main.rs"), DFLT_INIT_MAIN_RS, &opts.name)?; create_file_if_not_present( &dir.join("src/templates/mod.rs"), DFLT_INIT_MOD_RS, @@ -54,13 +54,13 @@ pub fn init(dir: PathBuf, opts: InitOpts) -> Result { /// Initializes a new Perseus project in a new directory that's a child of the /// current one. // The `dir` here is the current dir, the name of the one to create is in `opts` -pub fn new(dir: PathBuf, opts: NewOpts) -> Result { +pub fn new(dir: PathBuf, opts: &NewOpts, global_opts: &Opts) -> Result { // Create the directory (if the user provided a name explicitly, use that, // otherwise use the project name) - let target = dir.join(opts.dir.unwrap_or(opts.name.clone())); + let target = dir.join(opts.dir.as_ref().unwrap_or(&opts.name)); // Check if we're using the default template or one from a URL - if let Some(url) = opts.template { + if let Some(url) = &opts.template { let url_parts = url.split('@').collect::>(); let engine_url = url_parts[0]; // A custom branch can be specified after a `@`, or we'll use `stable` @@ -68,7 +68,7 @@ pub fn new(dir: PathBuf, opts: NewOpts) -> Result { // We'll only clone the production branch, and only the top level, we don't need the // whole shebang "{} clone --single-branch {branch} --depth 1 {repo} {output}", - std::env::var("PERSEUS_GIT_PATH").unwrap_or_else(|_| "git".to_string()), + global_opts.git_path, branch = if let Some(branch) = url_parts.get(1) { format!("--branch {}", branch) } else { @@ -103,7 +103,12 @@ pub fn new(dir: PathBuf, opts: NewOpts) -> Result { } else { fs::create_dir(&target).map_err(|err| NewError::CreateProjectDirFailed { source: err })?; // Now initialize in there - let exit_code = init(target, InitOpts { name: opts.name })?; + let exit_code = init( + target, + &InitOpts { + name: opts.name.to_string(), + }, + )?; Ok(exit_code) } } @@ -132,26 +137,11 @@ tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } perseus-warp = { version = "=0.4.0-beta.3", features = [ "dflt-server" ] } # Browser-only dependencies go here -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" - -# We'll use `src/lib.rs` as both a binary *and* a library at the same time (which we need to tell Cargo explicitly) -[lib] -name = "lib" -path = "src/lib.rs" -crate-type = [ "cdylib", "rlib" ] - -[[bin]] -name = "%name" -path = "src/lib.rs" - -# This section adds some optimizations to make your app nice and speedy in production -[package.metadata.wasm-pack.profile.release] -wasm-opt = [ "-Oz" ]"#; +[target.'cfg(target_arch = "wasm32")'.dependencies]"#; static DFLT_INIT_GITIGNORE: &str = r#"dist/ target_wasm/ target_engine/"#; -static DFLT_INIT_LIB_RS: &str = r#"mod templates; +static DFLT_INIT_MAIN_RS: &str = r#"mod templates; use perseus::{Html, PerseusApp}; diff --git a/packages/perseus-cli/src/install.rs b/packages/perseus-cli/src/install.rs new file mode 100644 index 0000000000..fef4d643a2 --- /dev/null +++ b/packages/perseus-cli/src/install.rs @@ -0,0 +1,616 @@ +use crate::cmd::{cfg_spinner, fail_spinner, succeed_spinner}; +use crate::errors::*; +use crate::parse::Opts; +use console::Emoji; +use flate2::read::GzDecoder; +use futures::future::try_join; +use home::home_dir; +use indicatif::ProgressBar; +use reqwest::Client; +use std::borrow::BorrowMut; +use std::fs; +use std::fs::File; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; +use tar::Archive; +use tokio::io::AsyncWriteExt; + +static INSTALLING: Emoji<'_, '_> = Emoji("📥", ""); + +// For each of the tools installed in this file, we preferentially +// manually download it. If that can't be achieved due to a platform +// mismatch, then we'll see if the user already has a verion installed. +// +// Importantly, if the user has specified an environment variable specifying +// where a tool can be found, we'll use that no matter what. + +/// Gets the directory to store tools in. This will preferentially use the +/// system-wide cache, falling back to a local version. +/// +/// If the user specifies that we're running on CI, we'll use the local version +/// regardless. +pub fn get_tools_dir(project: &Path, no_system_cache: bool) -> Result { + match home_dir() { + Some(path) if !no_system_cache => { + let target = path.join(".cargo/perseus_tools"); + if target.exists() { + Ok(target) + } else { + // Try to create the system-wide cache (this will create `~/.cargo/` as well if + // necessary) + if fs::create_dir_all(&target).is_ok() { + Ok(target) + } else { + // Failed, so we'll resort to the local cache + let target = project.join("dist/tools"); + if !target.exists() { + // If this fails, we have no recourse, so we'll have to fail + fs::create_dir_all(&target) + .map_err(|err| InstallError::CreateToolsDirFailed { source: err })?; + } + // It either already existed or we've just created it + Ok(target) + } + } + } + _ => { + let target = project.join("dist/tools"); + if !target.exists() { + // If this fails, we have no recourse, so we'll have to fail + fs::create_dir_all(&target) + .map_err(|err| InstallError::CreateToolsDirFailed { source: err })?; + } + // It either already existed or we've just created it + Ok(target) + } + } +} + +/// A representation of the paths to all the external tools we need. +/// This includes `cargo`, simply for convenience, even though it's not +/// actually independently installed. +/// +/// This does not contain metadata for the installation process, but is rather +/// intended to be passed around through the rest of the CLI. +#[derive(Clone)] +pub struct Tools { + /// The path to `cargo` on the engine-side. + pub cargo_engine: String, + /// The path to `cargo` on the browser-side. + pub cargo_browser: String, + /// The path to `wasm-bindgen`. + pub wasm_bindgen: String, + /// The path to `wasm-opt`. + pub wasm_opt: String, +} +impl Tools { + /// Gets a new instance of `Tools` by installing the tools if necessary. + /// + /// If tools are installed, this will create a CLI spinner automatically. + pub async fn new(dir: &Path, global_opts: &Opts) -> Result { + let target = get_tools_dir(dir, global_opts.no_system_tools_cache)?; + + // Instantiate the tools + let wasm_bindgen = Tool::new( + ToolType::WasmBindgen, + &global_opts.wasm_bindgen_path, + &global_opts.wasm_bindgen_version, + ); + let wasm_opt = Tool::new( + ToolType::WasmOpt, + &global_opts.wasm_opt_path, + &global_opts.wasm_opt_version, + ); + + // Get the statuses of all the tools + let wb_status = wasm_bindgen.get_status(&target)?; + let wo_status = wasm_opt.get_status(&target)?; + // Figure out if everything is present + // This is the only case in which we don't have to start the spinner + if let (ToolStatus::Available(wb_path), ToolStatus::Available(wo_path)) = + (&wb_status, &wo_status) + { + Ok(Tools { + cargo_engine: global_opts.cargo_engine_path.clone(), + cargo_browser: global_opts.cargo_browser_path.clone(), + wasm_bindgen: wb_path.to_string(), + wasm_opt: wo_path.to_string(), + }) + } else { + // We need to install some things, which may take some time + let spinner_msg = format!("{} Installing external tools", INSTALLING); + let spinner = cfg_spinner(ProgressBar::new_spinner(), &spinner_msg); + + // Install all the tools in parallel + // These functions sanity-check their statuses, so we don't need to worry about + // installing unnecessarily + let res = try_join( + wasm_bindgen.install(wb_status, &target), + wasm_opt.install(wo_status, &target), + ) + .await; + if let Err(err) = res { + fail_spinner(&spinner, &spinner_msg); + return Err(err); + } + // If we're here, we have the paths + succeed_spinner(&spinner, &spinner_msg); + let paths = res.unwrap(); + + Ok(Tools { + cargo_engine: global_opts.cargo_engine_path.clone(), + cargo_browser: global_opts.cargo_browser_path.clone(), + wasm_bindgen: paths.0, + wasm_opt: paths.1, + }) + } + } +} + +/// The data we need about an external tool to be able to install it. +/// +/// This does not contain data about arguments to these tools, that's passed +/// through the global options to the CLI directly (with defaults hardcoded). +pub struct Tool { + /// The name of the tool. This will also be used for the name of the + /// directory in `dist/tools/` in which the tool is stored (with the + /// version number added on). + pub name: String, + /// A path provided by the user to the tool. If this is present, then the + /// tool won't be installed, we'll use what the iuser has given us + /// instead. + pub user_given_path: Option, + /// A specific version number provided by the user. By default, the latest + /// version is used. + pub user_given_version: Option, + /// The path to the binary within the directory that is extracted from the + /// downloaded archive. + pub final_path: String, + /// The name of the GitHub repo from which the tool can be downloaded. + pub gh_repo: String, + /// The name of the directory that will be extracted. This should contain + /// `%version` to be replaced with the actual version and/or + /// `%artifact_name` to be replaced with that. + pub extracted_dir_name: String, + /// The actual type of the tool. (All tools have the same data.) + pub tool_type: ToolType, +} +impl Tool { + /// Creates a new instance of this `struct`. + pub fn new( + tool_type: ToolType, + user_given_path: &Option, + user_given_version: &Option, + ) -> Self { + // Correct the final path for Windows (on which it'll have a `.exe` extension) + #[cfg(unix)] + let final_path = tool_type.final_path(); + #[cfg(windows)] + let final_path = tool_type.final_path() + ".exe"; + + Self { + name: tool_type.name(), + user_given_path: user_given_path.to_owned(), + user_given_version: user_given_version.to_owned(), + final_path, + gh_repo: tool_type.gh_repo(), + extracted_dir_name: tool_type.extracted_dir_name(), + tool_type, + } + } + /// Gets the name of the artifact to download based on the tool data and the + /// version to download. Note that the version provided here entirely + /// overrides anything the user might have provided. + /// + /// If no precompiled binary is expected to be available for the current + /// platform, this will return `None`. + fn get_artifact_name(&self, version: &str) -> Option { + match &self.tool_type { + // --- `wasm-bindgen` --- + // Linux + ToolType::WasmBindgen if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") => { + Some("wasm-bindgen-%version-x86_64-unknown-linux-musl") + } + // MacOS (incl. Apple Silicon) + ToolType::WasmBindgen + if cfg!(target_os = "macos") + && (cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64")) => + { + Some("wasm-bindgen-%version-x86_64-apple-darwin") + } + // Windows + ToolType::WasmBindgen + if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") => + { + Some("wasm-bindgen-%version-x86_64-pc-windows-msvc") + } + ToolType::WasmBindgen => None, + // --- `wasm-opt` --- + // Linux + ToolType::WasmOpt if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") => { + Some("binaryen-%version-x86_64-linux") + } + // MacOS (Intel) + ToolType::WasmOpt if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") => { + Some("binaryen-%version-x86_64-macos") + } + // MacOS (Apple Silicon) + ToolType::WasmOpt if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") => { + Some("binaryen-%version-arm64-macos") + } + // Windows + ToolType::WasmOpt if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") => { + Some("binaryen-%version-x86_64-windows") + } + ToolType::WasmOpt => None, + } + .map(|s| s.replace("%version", version)) + } + /// Gets the path to the already-installed version of the tool to use. This + /// should take the full path to `dist/tools/`. This will automatically + /// handle whether or not to install a new version, use a verion already + /// installed globally on the user's system, etc. If this returns + /// `ToolStatus::NeedsInstall`, we can be sure that there are binaries + /// available, and the same if it returns `ToolStatus::NeedsLatestInstall`. + pub fn get_status(&self, target: &Path) -> Result { + // The status information will be incomplete from this first pass + let initial_status = { + // If there's a directory that matches with a given user version, we'll use it. + // If not, we'll use the latest version. Only if there are no + // installed versions available will this return `None`, or if the user wants a + // specific one that doesn't exist. + + // If the user has given us a path, that overrides everything + if let Some(path) = &self.user_given_path { + Ok(ToolStatus::Available(path.to_string())) + } else { + // If they've given us a version, we'll check if that directory exists (we don't + // care about any others) + if let Some(version) = &self.user_given_version { + let expected_path = target.join(format!("{}-{}", self.name, version)); + Ok(if fs::metadata(&expected_path).is_ok() { + ToolStatus::Available( + expected_path + .join(&self.final_path) + .to_string_lossy() + .to_string(), + ) + } else { + ToolStatus::NeedsInstall { + version: version.to_string(), + // This will be filled in on the second pass-through + artifact_name: String::new(), + } + }) + } else { + // We have no further information from the user, so we'll use the latest version + // that's installed, or we'll install the latest version. + // Either way, we need to know what we've got installed already by walking the + // directory. + let mut versions: Vec = Vec::new(); + for entry in fs::read_dir(target) + .map_err(|err| InstallError::ReadToolsDirFailed { source: err })? + { + let entry = entry + .map_err(|err| InstallError::ReadToolsDirFailed { source: err })?; + let dir_name = entry.file_name().to_string_lossy().to_string(); + if dir_name.starts_with(&self.name) { + let dir_name_ref = dir_name.to_string(); + // Valid directory names are of the form `-` + let version = dir_name + .strip_prefix(&format!("{}-", self.name)) + .ok_or(InstallError::InvalidToolsDirName { name: dir_name_ref })?; + versions.push(version.to_string()); + } + } + // Now order those from most recent to least recent + versions.sort(); + let versions = versions.into_iter().rev().collect::>(); + // If there are any at all, pick the first one + if !versions.is_empty() { + let latest_available_version = &versions[0]; + // We know the directory for this version had a valid name, so we can + // determine exactly where it was + let path_to_latest_version = target.join(format!( + "{}-{}/{}", + self.name, latest_available_version, self.final_path + )); + Ok(ToolStatus::Available( + path_to_latest_version.to_string_lossy().to_string(), + )) + } else { + // We don't check the latest version here because we haven't started the + // spinner yet + Ok(ToolStatus::NeedsLatestInstall) + } + } + } + }?; + // If we're considering installing something, we should make sure that there are + // actually precompiled binaries available for this platform (if there + // aren't, then we'll try to fall back on anything the user has installed + // locally, and if they have nothing, an error will be returned) + match initial_status { + ToolStatus::Available(path) => Ok(ToolStatus::Available(path)), + ToolStatus::NeedsInstall { version, .. } => { + // This will be `None` if there are no precompiled binaries available + let artifact_name = self.get_artifact_name(&version); + if let Some(artifact_name) = artifact_name { + // There are precompiled binaries available, which we prefer to preinstalled + // global ones + Ok(ToolStatus::NeedsInstall { + version, + artifact_name, + }) + } else { + // If the user has something, we're good, but, if not, we have to fail + let preinstalled_path = self.get_path_to_preinstalled()?; + // We've got something, but it might not be the right version, so if the user + // told us to use a specific version, we should warn them + if self.user_given_version.is_some() { + eprintln!("[WARNING]: You requested a specific version, but no precompiled binaries of '{}' are available for your platform, so the version already installed on your system is being used. This may not correspond to the requested version!", self.name); + } + Ok(ToolStatus::Available(preinstalled_path)) + } + } + ToolStatus::NeedsLatestInstall => { + // To get the proper artifact name for this, we would have to request the latest + // version, but we don't want to do that until a CLI spinner has + // been started, which is after the execution of this function (so we just use a + // dummy version to check if there would be binaries) + // This will be `None` if there are no precompiled binaries available + let artifact_name = self.get_artifact_name("dummy"); + if artifact_name.is_some() { + // There are precompiled binaries available, which we prefer to preinstalled + // global ones + Ok(ToolStatus::NeedsLatestInstall) + } else { + // If the user has something, we're good, but, if not, we have to fail + let preinstalled_path = self.get_path_to_preinstalled()?; + // The user can't have requested a specific version if we're in the market for + // the latest one, so we can wrap up here + Ok(ToolStatus::Available(preinstalled_path)) + } + } + } + } + /// Gets the latest version for this tool from its GitHub repository. One + /// should only bother executing this if we know there are precompiled + /// binaries for this platform. + pub async fn get_latest_version(&self) -> Result { + let json = Client::new() + .get(&format!( + "https://api.github.com/repos/{}/releases/latest", + self.gh_repo + )) + // TODO Is this compliant with GH's ToS? + .header("User-Agent", "perseus-cli") + .send() + .await + .map_err(|err| InstallError::GetLatestToolVersionFailed { + source: err, + tool: self.name.to_string(), + })? + .json::() + .await + .map_err(|err| InstallError::GetLatestToolVersionFailed { + source: err, + tool: self.name.to_string(), + })?; + let latest_version = + json.get("name") + .ok_or_else(|| InstallError::ParseToolVersionFailed { + tool: self.name.to_string(), + })?; + + Ok(latest_version + .as_str() + .ok_or_else(|| InstallError::ParseToolVersionFailed { + tool: self.name.to_string(), + })? + .to_string()) + } + /// Installs the tool, taking the predetermined status as an argument to + /// avoid installing if the tool is actually already available, since + /// this method will be called on all tools if even one + /// is not available. + pub async fn install(&self, status: ToolStatus, target: &Path) -> Result { + // Do a sanity check to prevent installing something that already exists + match status { + ToolStatus::Available(path) => Ok(path), + ToolStatus::NeedsInstall { + version, + artifact_name, + } => self.install_version(&version, &artifact_name, target).await, + ToolStatus::NeedsLatestInstall => { + let latest_version = self.get_latest_version().await?; + // We *do* know at this point that there do exist precompiled binaries + let artifact_name = self.get_artifact_name(&latest_version).unwrap(); + self.install_version(&latest_version, &artifact_name, target) + .await + } + } + } + /// Checks if the user already has this tool installed. This should only be + /// called if there are no precompiled binaries available of this tool + /// for the user's platform. In that case, this will also warn the user + /// if they've asked for a specific version that their own version might not + /// be that (we don't bother trying to parse the version of their + /// installed program). + /// + /// If there's nothing the user has installed, then this will return an + /// error, and hence it should onyl be called after all other options + /// have been exhausted. + fn get_path_to_preinstalled(&self) -> Result { + #[cfg(unix)] + let shell_exec = "sh"; + #[cfg(windows)] + let shell_exec = "powershell"; + #[cfg(unix)] + let shell_param = "-c"; + #[cfg(windows)] + let shell_param = "-command"; + + let check_cmd = format!("{} --version", self.name); // Not exactly bulletproof, but it works! + let res = Command::new(shell_exec) + .args([shell_param, &check_cmd]) + .output(); + if let Err(err) = res { + // Unlike `wasm-pack`, we don't try to install with `cargo install`, because + // that's a little pointless to me (the user will still have to get + // `wasm-opt` somehow...) + // + // TODO Installation script that can build manually on any platform + Err(InstallError::ExternalToolUnavailable { + tool: self.name.to_string(), + source: err, + }) + } else { + // It works, so we don't need to install anything + Ok(self.name.to_string()) + } + } + /// Installs the given version of the tool, returning the path to the final + /// binary. + async fn install_version( + &self, + version: &str, + artifact_name: &str, + target: &Path, + ) -> Result { + let url = format!( + "https://github.com/{gh_repo}/releases/download/{version}/{artifact_name}.tar.gz", + artifact_name = artifact_name, + version = version, + gh_repo = self.gh_repo + ); + let dir_name = format!("{}-{}", self.name, version); + let tar_name = format!("{}.tar.gz", &dir_name); + let dir_path = target.join(&dir_name); + let tar_path = target.join(&tar_name); + + // Deal with placeholders in the name to expect from the extracted directory + let extracted_dir_name = self + .extracted_dir_name + .replace("%artifact_name", artifact_name) + .replace("%version", version); + + // Download the tarball (source https://github.com/seanmonstar/reqwest/issues/1266#issuecomment-1106187437) + // We do this by chunking to minimize memory usage (we're downloading fairly + // large files!) + let mut res = Client::new().get(url).send().await.map_err(|err| { + InstallError::BinaryDownloadRequestFailed { + source: err, + tool: self.name.to_string(), + } + })?; + let mut file = tokio::fs::File::create(&tar_path) + .await + .map_err(|err| InstallError::CreateToolDownloadDestFailed { source: err })?; + while let Some(mut item) = res + .chunk() + .await + .map_err(|err| InstallError::ChunkBinaryDownloadFailed { source: err })? + { + file.write_all_buf(item.borrow_mut()) + .await + .map_err(|err| InstallError::WriteBinaryDownloadChunkFailed { source: err })?; + } + // Now unzip the tarball + // TODO Async? + let tar_gz = File::open(&tar_path) + .map_err(|err| InstallError::CreateToolExtractDestFailed { source: err })?; + let mut archive = Archive::new(GzDecoder::new(tar_gz)); + // We'll extract straight into `dist/tools/` and then rename the resulting + // directory + archive + .unpack(target) + .map_err(|err| InstallError::ToolExtractFailed { + source: err, + tool: self.name.to_string(), + })?; + + // Now delete the original archive file + fs::remove_file(&tar_path) + .map_err(|err| InstallError::ArchiveDeletionFailed { source: err })?; + // Finally, rename the extracted directory + fs::rename( + target.join(extracted_dir_name), // We extracted into the root of the target + &dir_path, + ) + .map_err(|err| InstallError::DirRenameFailed { source: err })?; + + // Return the path inside the directory we extracted + Ok(dir_path + .join(&self.final_path) + .to_str() + .unwrap() + .to_string()) + } +} + +/// A tool's status on-system. +pub enum ToolStatus { + /// The tool needs to be installed. + NeedsInstall { + version: String, + artifact_name: String, + }, + /// The latest version of the tool needs to be determined from its repo and + /// then installed. + NeedsLatestInstall, + /// The tool is already available at the attached path. + Available(String), +} + +/// The types of tools we can install. +pub enum ToolType { + /// The `wasm-bindgen` CLI, used for producing final Wasm and JS artifacts. + WasmBindgen, + /// Binaryen's `wasm-opt` CLI, used for optimizing Wasm in release builds + /// to achieve significant savings in bundle sizes. + WasmOpt, +} +impl ToolType { + /// Gets the tool's name. + pub fn name(&self) -> String { + match &self { + Self::WasmBindgen => "wasm-bindgen", + Self::WasmOpt => "wasm-opt", + } + .to_string() + } + /// Get's the path to the tool's binary inside the extracted directory + /// from the downloaded archive. + /// + /// Note that the return value of this function is uncorrected for Windows. + pub fn final_path(&self) -> String { + match &self { + Self::WasmBindgen => "wasm-bindgen", + Self::WasmOpt => "bin/wasm-opt", + } + .to_string() + } + /// Gets the GitHub repo to install this tool from. + pub fn gh_repo(&self) -> String { + match &self { + Self::WasmBindgen => "rustwasm/wasm-bindgen", + Self::WasmOpt => "WebAssembly/binaryen", + } + .to_string() + } + /// Gets the name of the directory that will be extracted from the + /// downloaded archive for this tool. + /// + /// This will return a `String` that might include placeholders for the + /// version and downloaded artifact name. + pub fn extracted_dir_name(&self) -> String { + match &self { + Self::WasmBindgen => "%artifact_name", + Self::WasmOpt => "binaryen-%version", + } + .to_string() + } +} diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index 5c70edc90c..06887d1d8f 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -22,6 +22,7 @@ pub mod errors; mod export; mod export_error_page; mod init; +mod install; /// Parsing utilities for arguments. pub mod parse; mod prepare; @@ -33,8 +34,8 @@ mod thread; mod tinker; use errors::*; -use std::fs; use std::path::PathBuf; +use std::{fs, path::Path}; /// The current version of the CLI, extracted from the crate version. pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -43,6 +44,7 @@ pub use deploy::deploy; pub use export::export; pub use export_error_page::export_error_page; pub use init::{init, new}; +pub use install::{get_tools_dir, Tools}; pub use prepare::check_env; pub use reload_server::{order_reload, run_reload_server}; pub use serve::serve; @@ -50,8 +52,19 @@ pub use serve_exported::serve_exported; pub use snoop::{snoop_build, snoop_server, snoop_wasm_build}; pub use tinker::tinker; -/// Deletes the entire `dist/` directory. Nicely, because there are no Cargo -/// artifacts in there, running this won't slow down future runs at all. +/// Creates the `dist/` directory in the project root, which is necessary +/// for Cargo to be able to put its build artifacts in there. +pub fn create_dist(dir: &Path) -> Result<(), ExecutionError> { + let target = dir.join("dist"); + if !target.exists() { + fs::create_dir(target).map_err(|err| ExecutionError::CreateDistFailed { source: err })?; + } + Ok(()) +} + +/// Deletes the entire `dist/` directory. Notably, this is where we keep +/// several Cargo artifacts, so this means the next build will be much +/// slower. pub fn delete_dist(dir: PathBuf) -> Result<(), ExecutionError> { let target = dir.join("dist"); if target.exists() { @@ -92,3 +105,15 @@ pub fn delete_artifacts(dir: PathBuf, dir_to_remove: &str) -> Result<(), Executi Ok(()) } + +/// Gets the name of the user's crate from their `Cargo.toml` (assumed to be in +/// the root of the given directory). +pub fn get_user_crate_name(dir: &Path) -> Result { + let manifest = cargo_toml::Manifest::from_path(dir.join("Cargo.toml")) + .map_err(|err| ExecutionError::GetManifestFailed { source: err })?; + let name = manifest + .package + .ok_or(ExecutionError::CrateNameNotPresentInManifest)? + .name; + Ok(name) +} diff --git a/packages/perseus-cli/src/parse.rs b/packages/perseus-cli/src/parse.rs index 4196ca1006..6ed04037d0 100644 --- a/packages/perseus-cli/src/parse.rs +++ b/packages/perseus-cli/src/parse.rs @@ -8,15 +8,80 @@ use clap::Parser; /// The command-line interface for Perseus, a super-fast WebAssembly frontend /// development framework! -#[derive(Parser)] +#[derive(Parser, Clone)] #[clap(version = PERSEUS_VERSION)] // #[clap(setting = AppSettings::ColoredHelp)] pub struct Opts { #[clap(subcommand)] pub subcmd: Subcommand, + // All the following arguments are global, and can provided to any subcommand + /// The path to `cargo` when used for engine builds + #[clap(long, default_value = "cargo", global = true)] + pub cargo_engine_path: String, + /// The path to `cargo` when used for browser builds + #[clap(long, default_value = "cargo", global = true)] + pub cargo_browser_path: String, + /// A path to `wasm-bindgen`, if you want to use a local installation (note + /// that the CLI will install it locally for you by default) + #[clap(long, global = true)] + pub wasm_bindgen_path: Option, + /// A path to `wasm-opt`, if you want to use a local installation (note that + /// the CLI will install it locally for you by default) + #[clap(long, global = true)] + pub wasm_opt_path: Option, + /// The path to `rustup` + #[clap(long, default_value = "rustup", global = true)] + pub rustup_path: String, + /// The value of `RUSTFLAGS` when building for Wasm in release mode + #[clap( + long, + default_value = "-C opt-level=z -C codegen-units=1", + global = true + )] + pub wasm_release_rustflags: String, + /// Any arguments to `cargo` when building for the engine-side + #[clap(long, default_value = "", global = true)] + pub cargo_engine_args: String, + /// Any arguments to `cargo` when building for the browser-side + #[clap(long, default_value = "", global = true)] + pub cargo_browser_args: String, + /// Any arguments to `wasm-bindgen` + #[clap(long, default_value = "", global = true)] + pub wasm_bindgen_args: String, + /// Any arguments to `wasm-opt` (only run in release builds) + #[clap(long, default_value = "-Oz", global = true)] + pub wasm_opt_args: String, + /// The path to `git` (for downloading custom templates for `perseus new`) + #[clap(long, default_value = "git", global = true)] + pub git_path: String, + /// The host for the reload server (you should almost never change this) + #[clap(long, default_value = "localhost", global = true)] + pub reload_server_host: String, + /// The port for the reload server (you should almost never change this) + #[clap(long, default_value = "3100", global = true)] + pub reload_server_port: u16, + /// If this is set, commands will be run sequentially rather than in + /// parallel (slows down operations, but reduces memory usage) + #[clap(long, global = true)] + pub sequential: bool, + /// Disable automatic browser reloading + #[clap(long, global = true)] + pub no_browser_reload: bool, + /// A custom version of `wasm-bindgen` to use (defaults to the latest + /// installed version, and after that the latest available from GitHub) + #[clap(long, global = true)] + pub wasm_bindgen_version: Option, + /// A custom version of `wasm-opt` to use (defaults to the latest installed + /// version, and after that the latest available from GitHub) + #[clap(long, global = true)] + pub wasm_opt_version: Option, + /// Disables the system-wide tools cache in `~/.cargo/perseus_tools/` (you + /// should set this for CI) + #[clap(long, global = true)] + pub no_system_tools_cache: bool, } -#[derive(Parser)] +#[derive(Parser, Clone)] pub enum Subcommand { Build(BuildOpts), ExportErrorPage(ExportErrorPageOpts), @@ -36,7 +101,7 @@ pub enum Subcommand { Init(InitOpts), } /// Builds your app -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct BuildOpts { /// Build for production #[clap(long)] @@ -108,7 +173,7 @@ pub struct ServeOpts { pub port: u16, } /// Packages your app for deployment -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct DeployOpts { /// Change the output from `pkg/` to somewhere else #[clap(short, long, default_value = "pkg")] @@ -119,7 +184,7 @@ pub struct DeployOpts { } /// Runs the `tinker` action of plugins, which lets them modify the Perseus /// engine -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct TinkerOpts { /// Don't remove and recreate the `dist/` directory #[clap(long)] @@ -127,7 +192,7 @@ pub struct TinkerOpts { } /// Creates a new Perseus project in a directory of the given name, which will /// be created in the current path -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct NewOpts { /// The name of the new project, which will also be used for the directory #[clap(value_parser)] @@ -144,32 +209,25 @@ pub struct NewOpts { pub dir: Option, } /// Intializes a new Perseus project in the current directory -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct InitOpts { /// The name of the new project #[clap(value_parser)] pub name: String, } -#[derive(Parser)] +#[derive(Parser, Clone)] pub enum SnoopSubcommand { /// Snoops on the static generation process (this will let you see `dbg!` /// calls and the like) Build, /// Snoops on the Wasm building process (mostly for debugging errors) - WasmBuild(SnoopWasmOpts), + WasmBuild, /// Snoops on the server process (run `perseus build` before this) Serve(SnoopServeOpts), } -#[derive(Parser)] -pub struct SnoopWasmOpts { - /// Produce a profiling build (for use with `twiggy` and the like) - #[clap(short, long)] - pub profiling: bool, -} - -#[derive(Parser)] +#[derive(Parser, Clone)] pub struct SnoopServeOpts { /// Where to host your exported app #[clap(long, default_value = "127.0.0.1")] diff --git a/packages/perseus-cli/src/prepare.rs b/packages/perseus-cli/src/prepare.rs index ed63ce4129..62069171f2 100644 --- a/packages/perseus-cli/src/prepare.rs +++ b/packages/perseus-cli/src/prepare.rs @@ -1,7 +1,7 @@ +use crate::cmd::run_cmd_directly; use crate::errors::*; -#[allow(unused_imports)] -use cargo_toml::Manifest; -use std::env; +use crate::parse::Opts; +use std::path::PathBuf; use std::process::Command; /// Checks if the user has the necessary prerequisites on their system (i.e. @@ -9,33 +9,60 @@ use std::process::Command; /// their binaries and looking for errors. If the user has other paths for /// these, they can define them under the environment variables /// `PERSEUS_CARGO_PATH` and `PERSEUS_WASM_PACK_PATH`. -pub fn check_env() -> Result<(), Error> { - // We'll loop through each prerequisite executable to check their existence - // If the spawn returns an error, it's considered not present, success means - // presence - let prereq_execs = vec![ - ( - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - "cargo", - "PERSEUS_CARGO_PATH", - ), - ( - env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - "wasm-pack", - "PERSEUS_WASM_PACK_PATH", - ), - ]; +/// +/// Checks if the user has `cargo` installed, and tries to install the +/// `wasm32-unknown-unknown` target with `rustup` if it's available. +pub fn check_env(global_opts: &Opts) -> Result<(), Error> { + #[cfg(unix)] + let shell_exec = "sh"; + #[cfg(windows)] + let shell_exec = "powershell"; + #[cfg(unix)] + let shell_param = "-c"; + #[cfg(windows)] + let shell_param = "-command"; - for exec in prereq_execs { - let res = Command::new(&exec.0).output(); - // Any errors are interpreted as meaning that the user doesn't have the - // prerequisite installed properly. - if let Err(err) = res { - return Err(Error::PrereqNotPresent { - cmd: exec.1.to_string(), - env_var: exec.2.to_string(), - source: err, - }); + // Check for `cargo` + let cargo_cmd = global_opts.cargo_engine_path.to_string() + " --version"; + let cargo_res = Command::new(shell_exec) + .args([shell_param, &cargo_cmd]) + .output() + .map_err(|err| Error::CargoNotPresent { source: err })?; + let exit_code = match cargo_res.status.code() { + Some(exit_code) => exit_code, + None if cargo_res.status.success() => 0, + None => 1, + }; + if exit_code != 0 { + return Err(Error::CargoNotPresent { + source: std::io::Error::new(std::io::ErrorKind::NotFound, "non-zero exit code"), + }); + } + // If the user has `rustup`, make sure they have `wasm32-unknown-unknown` + // installed If they don'aren't using `rustup`, we won't worry about this + let rustup_cmd = global_opts.rustup_path.to_string() + " target list"; + let rustup_res = Command::new(shell_exec) + .args([shell_param, &rustup_cmd]) + .output(); + if let Ok(rustup_res) = rustup_res { + let exit_code = match rustup_res.status.code() { + Some(exit_code) => exit_code, + None if rustup_res.status.success() => 0, + None => 1, + }; + if exit_code == 0 { + let stdout = String::from_utf8_lossy(&rustup_res.stdout); + let has_wasm_target = stdout.contains("wasm32-unknown-unknown (installed)"); + if !has_wasm_target { + let exit_code = run_cmd_directly( + "rustup target add wasm32-unknown-unknown".to_string(), + &PathBuf::from("."), + vec![], + )?; + if exit_code != 0 { + return Err(Error::RustupTargetAddFailed { code: exit_code }); + } + } } } diff --git a/packages/perseus-cli/src/reload_server.rs b/packages/perseus-cli/src/reload_server.rs index 6773895690..bab1b559f5 100644 --- a/packages/perseus-cli/src/reload_server.rs +++ b/packages/perseus-cli/src/reload_server.rs @@ -21,9 +21,7 @@ static NEXT_UID: AtomicUsize = AtomicUsize::new(0); /// Runs the reload server, which is used to instruct the browser on when to /// reload for updates. -pub async fn run_reload_server() { - let (host, port) = get_reload_server_host_and_port(); - +pub async fn run_reload_server(host: String, port: u16) { // Parse `localhost` into `127.0.0.1` (picky Rust `std`) let host = if host == "localhost" { "127.0.0.1".to_string() @@ -93,25 +91,13 @@ pub async fn run_reload_server() { /// Orders all connected browsers to reload themselves. This spawns a blocking /// task through Tokio under the hood. Note that this will only do anything if /// `PERSEUS_USE_RELOAD_SERVER` is set to `true`. -pub fn order_reload() { +pub fn order_reload(host: String, port: u16) { + // This environment variable is only for use by the CLI internally if env::var("PERSEUS_USE_RELOAD_SERVER").is_ok() { - let (host, port) = get_reload_server_host_and_port(); - - tokio::task::spawn_blocking(move || { + tokio::task::spawn(async move { // We don't care if this fails because we have no guarnatees that the server is // actually up - let _ = ureq::get(&format!("http://{}:{}/send", host, port)).call(); + let _ = reqwest::get(&format!("http://{}:{}/send", host, port)).await; }); } } - -/// Gets the host and port to run the reload server on. -fn get_reload_server_host_and_port() -> (String, u16) { - let host = env::var("PERSEUS_RELOAD_SERVER_HOST").unwrap_or_else(|_| "localhost".to_string()); - let port = env::var("PERSEUS_RELOAD_SERVER_PORT").unwrap_or_else(|_| "3100".to_string()); - let port = port - .parse::() - .expect("reload server port must be a number"); - - (host, port) -} diff --git a/packages/perseus-cli/src/serve.rs b/packages/perseus-cli/src/serve.rs index a4d2ff0fd5..52d4fdf96e 100644 --- a/packages/perseus-cli/src/serve.rs +++ b/packages/perseus-cli/src/serve.rs @@ -1,6 +1,7 @@ use crate::build::build_internal; use crate::cmd::{cfg_spinner, run_stage}; -use crate::parse::ServeOpts; +use crate::install::Tools; +use crate::parse::{Opts, ServeOpts}; use crate::thread::{spawn_thread, ThreadHandle}; use crate::{errors::*, order_reload}; use console::{style, Emoji}; @@ -38,10 +39,16 @@ fn build_server( did_build: bool, exec: Arc>, is_release: bool, + tools: &Tools, + global_opts: &Opts, ) -> Result< ThreadHandle Result, Result>, ExecutionError, > { + let tools = tools.clone(); + let Opts { + cargo_engine_args, .. + } = global_opts.clone(); let num_steps = match did_build { true => 4, false => 2, @@ -62,35 +69,37 @@ fn build_server( let sb_spinner = spinners.insert(num_steps - 1, ProgressBar::new_spinner()); let sb_spinner = cfg_spinner(sb_spinner, &sb_msg); let sb_target = dir; - let sb_thread = spawn_thread(move || { - let (stdout, _stderr) = handle_exit_code!(run_stage( - vec![&format!( - // This sets Cargo to tell us everything, including the executable path to the - // server - "{} build --message-format json {} {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - if is_release { "--release" } else { "" }, - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) - )], - &sb_target, - &sb_spinner, - &sb_msg, - vec![] - )?); - - let msgs: Vec<&str> = stdout.trim().split('\n').collect(); - // If we got to here, the exit code was 0 and everything should've worked - // The last message will just tell us that the build finished, the second-last - // one will tell us the executable path - let msg = msgs.get(msgs.len() - 2); - let msg = match msg { - // We'll parse it as a Serde `Value`, we don't need to know everything that's in there - Some(msg) => serde_json::from_str::(msg) - .map_err(|err| ExecutionError::GetServerExecutableFailed { source: err })?, - None => return Err(ExecutionError::ServerExectutableMsgNotFound), - }; - let server_exec_path = msg.get("executable"); - let server_exec_path = match server_exec_path { + let sb_thread = spawn_thread( + move || { + let (stdout, _stderr) = handle_exit_code!(run_stage( + vec![&format!( + // This sets Cargo to tell us everything, including the executable path to the + // server + "{} build --message-format json {} {}", + tools.cargo_engine, + if is_release { "--release" } else { "" }, + cargo_engine_args + )], + &sb_target, + &sb_spinner, + &sb_msg, + vec![("CARGO_TARGET_DIR", "dist/target_engine")] + )?); + + let msgs: Vec<&str> = stdout.trim().split('\n').collect(); + // If we got to here, the exit code was 0 and everything should've worked + // The last message will just tell us that the build finished, the second-last + // one will tell us the executable path + let msg = msgs.get(msgs.len() - 2); + let msg = match msg { + // We'll parse it as a Serde `Value`, we don't need to know everything that's in + // there + Some(msg) => serde_json::from_str::(msg) + .map_err(|err| ExecutionError::GetServerExecutableFailed { source: err })?, + None => return Err(ExecutionError::ServerExectutableMsgNotFound), + }; + let server_exec_path = msg.get("executable"); + let server_exec_path = match server_exec_path { // We'll parse it as a Serde `Value`, we don't need to know everything that's in there Some(server_exec_path) => match server_exec_path.as_str() { Some(server_exec_path) => server_exec_path, @@ -106,12 +115,14 @@ fn build_server( }), }; - // And now the main thread needs to know about this - let mut exec_val = exec.lock().unwrap(); - *exec_val = server_exec_path.to_string(); + // And now the main thread needs to know about this + let mut exec_val = exec.lock().unwrap(); + *exec_val = server_exec_path.to_string(); - Ok(0) - }); + Ok(0) + }, + global_opts.sequential, + ); Ok(sb_thread) } @@ -194,9 +205,15 @@ fn run_server( /// Builds the subcrates to get a directory that we can serve and then serves /// it. If possible, this will return the path to the server executable so that /// it can be used in deployment. -pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), ExecutionError> { +pub fn serve( + dir: PathBuf, + opts: &ServeOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result<(i32, Option), ExecutionError> { // Set the environment variables for the host and port - env::set_var("PERSEUS_HOST", opts.host); + // NOTE Another part of this code depends on setting these in this way + env::set_var("PERSEUS_HOST", &opts.host); env::set_var("PERSEUS_PORT", opts.port.to_string()); let spinners = MultiProgress::new(); @@ -212,10 +229,13 @@ pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), Exe did_build, Arc::clone(&exec), opts.release, + tools, + global_opts, )?; // Only build if the user hasn't set `--no-build`, handling non-zero exit codes if did_build { - let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 4, opts.release)?; + let (sg_thread, wb_thread) = + build_internal(dir.clone(), &spinners, 4, opts.release, tools, global_opts)?; let sg_res = sg_thread .join() .map_err(|_| ExecutionError::ThreadWaitFailed)??; @@ -237,7 +257,10 @@ pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), Exe } // Order any connected browsers to reload - order_reload(); + order_reload( + global_opts.reload_server_host.to_string(), + global_opts.reload_server_port, + ); // Now actually run that executable path if we should if should_run { diff --git a/packages/perseus-cli/src/snoop.rs b/packages/perseus-cli/src/snoop.rs index f6379f232a..159b2beea9 100644 --- a/packages/perseus-cli/src/snoop.rs +++ b/packages/perseus-cli/src/snoop.rs @@ -1,63 +1,79 @@ use crate::cmd::run_cmd_directly; -use crate::errors::*; -use crate::parse::{SnoopServeOpts, SnoopWasmOpts}; -use std::env; +use crate::install::Tools; +use crate::parse::{Opts, SnoopServeOpts}; +use crate::{errors::*, get_user_crate_name}; use std::path::PathBuf; /// Runs static generation processes directly so the user can see detailed logs. /// This is commonly used for allowing users to see `dbg!` and the like in their /// builder functions. -pub fn snoop_build(dir: PathBuf) -> Result { +pub fn snoop_build(dir: PathBuf, tools: &Tools, global_opts: &Opts) -> Result { run_cmd_directly( format!( "{} run {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) + tools.cargo_engine, global_opts.cargo_engine_args ), &dir, vec![ ("PERSEUS_ENGINE_OPERATION", "build"), - ("CARGO_TARGET_DIR", "target_engine"), + ("CARGO_TARGET_DIR", "dist/target_engine"), ], ) } /// Runs the commands to build the user's app to Wasm directly so they can see -/// detailed logs. -pub fn snoop_wasm_build(dir: PathBuf, opts: SnoopWasmOpts) -> Result { +/// detailed logs. This can't be used for release builds, so we don't have to +/// worry about `wasm-opt`. +pub fn snoop_wasm_build( + dir: PathBuf, + tools: &Tools, + global_opts: &Opts, +) -> Result { + let crate_name = get_user_crate_name(&dir)?; + + println!("[NOTE]: You should expect unused code warnings here! Don't worry about them, they're just a product of the target-gating."); + let exit_code = run_cmd_directly( + format!( + "{} build --target wasm32-unknown-unknown {}", + tools.cargo_browser, global_opts.cargo_browser_args + ), + &dir, + vec![("CARGO_TARGET_DIR", "dist/target_wasm")], + )?; + if exit_code != 0 { + return Ok(exit_code); + } run_cmd_directly( format!( - "{} build --out-dir dist/pkg --out-name perseus_engine --target web {} {}", - env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - if opts.profiling { - "--profiling" - } else { - "--dev" - }, - env::var("PERSEUS_WASM_PACK_ARGS").unwrap_or_else(|_| String::new()) + "{cmd} ./dist/target_wasm/wasm32-unknown-unknown/debug/{crate_name}.wasm --out-dir dist/pkg --out-name perseus_engine --target web {args}", + cmd=tools.wasm_bindgen, + args=global_opts.wasm_bindgen_args, + crate_name=crate_name ), &dir, - vec![("CARGO_TARGET_DIR", "target_wasm")], + vec![("CARGO_TARGET_DIR", "dist/target_wasm")], ) } /// Runs the commands to run the server directly so the user can see detailed /// logs. -pub fn snoop_server(dir: PathBuf, opts: SnoopServeOpts) -> Result { - // Set the environment variables for the host and port - env::set_var("PERSEUS_HOST", opts.host); - env::set_var("PERSEUS_PORT", opts.port.to_string()); - +pub fn snoop_server( + dir: PathBuf, + opts: &SnoopServeOpts, + tools: &Tools, + global_opts: &Opts, +) -> Result { run_cmd_directly( format!( "{} run {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) + tools.cargo_engine, global_opts.cargo_engine_args ), &dir, vec![ ("PERSEUS_ENGINE_OPERATION", "serve"), - ("CARGO_TARGET_DIR", "target_engine"), + ("CARGO_TARGET_DIR", "dist/target_engine"), + ("PERSEUS_HOST", &opts.host), + ("PERSEUS_PORT", &opts.port.to_string()), ], /* Unlike the `serve` command, we're both * building and running here, so we provide * the operation */ diff --git a/packages/perseus-cli/src/thread.rs b/packages/perseus-cli/src/thread.rs index b1de3d19ab..85847d5c93 100644 --- a/packages/perseus-cli/src/thread.rs +++ b/packages/perseus-cli/src/thread.rs @@ -1,17 +1,15 @@ -use std::env; use std::thread::{self, JoinHandle}; /// Spawns a new thread with the given code, or executes it directly if the /// environment variable `PERSEUS_CLI_SEQUENTIAL` is set to any valid (Unicode) /// value. Multithreading is the default. -pub fn spawn_thread(f: F) -> ThreadHandle +pub fn spawn_thread(f: F, sequential: bool) -> ThreadHandle where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, { - let single = env::var("PERSEUS_CLI_SEQUENTIAL").is_ok(); - if single { + if sequential { ThreadHandle { join_handle: None, f: Some(f), diff --git a/packages/perseus-cli/src/tinker.rs b/packages/perseus-cli/src/tinker.rs index 2e3edf2961..afab34a385 100644 --- a/packages/perseus-cli/src/tinker.rs +++ b/packages/perseus-cli/src/tinker.rs @@ -1,9 +1,10 @@ use crate::cmd::{cfg_spinner, run_stage}; use crate::errors::*; +use crate::install::Tools; +use crate::parse::Opts; use crate::thread::{spawn_thread, ThreadHandle}; use console::{style, Emoji}; use indicatif::{MultiProgress, ProgressBar}; -use std::env; use std::path::PathBuf; // Emojis for stages @@ -29,10 +30,17 @@ pub fn tinker_internal( dir: PathBuf, spinners: &MultiProgress, num_steps: u8, + tools: &Tools, + global_opts: &Opts, ) -> Result< ThreadHandle Result, Result>, Error, > { + let tools = tools.clone(); + let Opts { + cargo_engine_args, .. + } = global_opts.clone(); + // Tinkering message let tk_msg = format!( "{} {} Running plugin tinkers", @@ -45,21 +53,23 @@ pub fn tinker_internal( let tk_spinner = spinners.insert(0, ProgressBar::new_spinner()); let tk_spinner = cfg_spinner(tk_spinner, &tk_msg); let tk_target = dir; - let tk_thread = spawn_thread(move || { - handle_exit_code!(run_stage( - vec![&format!( - "{} run {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) - )], - &tk_target, - &tk_spinner, - &tk_msg, - vec![("PERSEUS_ENGINE_OPERATION", "tinker")] - )?); + let tk_thread = spawn_thread( + move || { + handle_exit_code!(run_stage( + vec![&format!("{} run {}", tools.cargo_engine, cargo_engine_args)], + &tk_target, + &tk_spinner, + &tk_msg, + vec![ + ("PERSEUS_ENGINE_OPERATION", "tinker"), + ("CARGO_TARGET_DIR", "dist/target_engine") + ] + )?); - Ok(0) - }); + Ok(0) + }, + global_opts.sequential, + ); Ok(tk_thread) } @@ -67,10 +77,10 @@ pub fn tinker_internal( /// Runs plugin tinkers on the engine and returns an exit code. This doesn't /// have a release mode because tinkers should be applied in development to work /// in both development and production. -pub fn tinker(dir: PathBuf) -> Result { +pub fn tinker(dir: PathBuf, tools: &Tools, global_opts: &Opts) -> Result { let spinners = MultiProgress::new(); - let tk_thread = tinker_internal(dir, &spinners, 1)?; + let tk_thread = tinker_internal(dir, &spinners, 1, tools, global_opts)?; let tk_res = tk_thread .join() .map_err(|_| ExecutionError::ThreadWaitFailed)??; diff --git a/packages/perseus-cli/tests/README.md b/packages/perseus-cli/tests/README.md new file mode 100644 index 0000000000..b9469aa9be --- /dev/null +++ b/packages/perseus-cli/tests/README.md @@ -0,0 +1,45 @@ +# CLI Tests + +This directory contains a series of integration tests written for the CLI's end-to-end behavior. These do not test the features of Perseus (integration tests in the `examples/` directory do that), these use only the inbuilt example generated by `perseus new`. + +## Tests + +- `perseus new` + - With just a project name (this should then be able to `perseus build`) + - With a project name and an arbitrary template URL + - With a project name and an arbitrary template URL pointing to a specific branch +- `perseus init` +- `perseus clean` +- `perseus build` + - First time, ensuring tools are installed + - Second time with same directory, ensuring tools are not reinstalled + - In release mode, ensuring smaller artifacts produced +- `perseus export` + - Without serving + - With serving + - Serving with custom host/port + - In release mode, ensuring smaller artifacts produced +- `perseus export-error-page` + - Error code for which a page is defined + - Error code for which a page is not defined (fallback should be rendered) +- `perseus serve`/`perseus test` (same code, `perseus test`'s unique behavior tested in `examples/` integration tests by proxy) + - Default + - With custom host/port + - In release mode, ensuring smaller artifacts produced + - Without running final binary (confirming that produced path to binary exists) +- `perseus snoop build` (checking `dbg!` call visibility) +- `perseus snoop wasm-build` (checking `dbg!` call visibility) +- `perseus snoop serve` (checking `dbg!` call visibility) + - Default + - With custom host/port +- `perseus tinker` (with a simple plugin that generates a file) + +- Watching with `perseus serve` (behavior identical in code to `perseus export`) + - Watching custom files/directories + +## Untested Behavior + +These behaviors are those known to be untested. Any behaviors not listed here that aren't tested most certainly should be, and you should [file an issue](https://github.com/artic-hen7/perseus/issues/new/choose)! + +- System-wide tools cache (TODO with spoofed home directory) +- Browser reloading/HSR (manually tested) diff --git a/packages/perseus-cli/tests/new.rs b/packages/perseus-cli/tests/new.rs new file mode 100644 index 0000000000..bb70a66679 --- /dev/null +++ b/packages/perseus-cli/tests/new.rs @@ -0,0 +1,19 @@ +use assert_cmd::prelude::*; +use assert_fs::TempDir; +use predicates::prelude::*; +use std::process::Command; + +/// Makes sure `perseus new` successfully generates the hardcoded example. +// TODO +#[test] +fn default() -> Result<(), Box> { + let dir = TempDir::new()?; + let mut cmd = Command::cargo_bin("perseus")?; + cmd.env("TEST_EXAMPLE", dir.path()) // In dev, the CLI can be made to run anywhere! + .arg("--help"); + cmd.assert() + .success() + .stdout(predicate::str::contains("perseus [OPTIONS] ")); + + Ok(()) +} diff --git a/packages/perseus-macro/src/entrypoint.rs b/packages/perseus-macro/src/entrypoint.rs index 81cb6250c2..ffdf83a19b 100644 --- a/packages/perseus-macro/src/entrypoint.rs +++ b/packages/perseus-macro/src/entrypoint.rs @@ -181,7 +181,6 @@ pub fn main_impl(input: MainFn, server_fn: Path) -> TokenStream { // The browser-specific `main` function #[cfg(target_arch = "wasm32")] - #[wasm_bindgen::prelude::wasm_bindgen] pub fn main() -> ::perseus::ClientReturn { ::perseus::run_client(__perseus_simple_main) } @@ -220,7 +219,6 @@ pub fn main_export_impl(input: MainFn) -> TokenStream { // The browser-specific `main` function #[cfg(target_arch = "wasm32")] - #[wasm_bindgen::prelude::wasm_bindgen] pub fn main() -> ::perseus::ClientReturn { ::perseus::run_client(__perseus_simple_main) } @@ -250,7 +248,6 @@ pub fn browser_main_impl(input: MainFn) -> TokenStream { // The browser-specific `main` function // This absolutely MUST be called `main`, otherwise the hardcodes Wasm importer will fail (and then interactivity is gone completely with a really weird error message) #[cfg(target_arch = "wasm32")] - #[wasm_bindgen::prelude::wasm_bindgen] #(#attrs)* pub fn main() -> #return_type { #block diff --git a/packages/perseus/src/engine/get_op.rs b/packages/perseus/src/engine/get_op.rs index bd6cbefcb4..1ddd52e8ee 100644 --- a/packages/perseus/src/engine/get_op.rs +++ b/packages/perseus/src/engine/get_op.rs @@ -16,7 +16,6 @@ pub fn get_op() -> Option { // we shouldn't need to specify this environment variable So, in // production, we take the server as the default If a user wants // a builder though, they can just set the environment variable - // TODO Document this! if cfg!(debug_assertions) { None } else { diff --git a/packages/perseus/src/engine/serve.rs b/packages/perseus/src/engine/serve.rs index f8c8e3b535..7206d0d7f1 100644 --- a/packages/perseus/src/engine/serve.rs +++ b/packages/perseus/src/engine/serve.rs @@ -27,9 +27,21 @@ pub(crate) fn get_host_and_port() -> (String, u16) { /// Gets the properties to pass to the server, invoking plugin opportunities as /// necessary. This is entirely engine-agnostic. +/// +/// WARNING: in production, this will automatically set the working directory +/// to be the parent of the actual binary! This means that disabling +/// debug assertions in development will lead to utterly incomprehensible +/// errors! You have been warned! pub(crate) fn get_props( app: PerseusAppBase, ) -> ServerProps { + if !cfg!(debug_assertions) { + let binary_loc = env::current_exe().unwrap(); + let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close + // to sanity + env::set_current_dir(binary_dir).unwrap(); + } + let plugins = app.get_plugins(); plugins diff --git a/packages/perseus/src/server/html_shell.rs b/packages/perseus/src/server/html_shell.rs index d81d2f6f0d..5174f897d4 100644 --- a/packages/perseus/src/server/html_shell.rs +++ b/packages/perseus/src/server/html_shell.rs @@ -77,13 +77,15 @@ impl HtmlShell { // unnecessary extra requests) If we're using the `wasm2js` feature, // this will try to load a JS version instead (expected to be at // `/.perseus/bundle.wasm.js`) + // + // Note: because we're using binary bundles, we don't need to import + // a `main` function or the like, `init()` just works #[cfg(not(feature = "wasm2js"))] let load_wasm_bundle = format!( r#" - import init, {{ main as run }} from "{path_prefix}/.perseus/bundle.js"; + import init from "{path_prefix}/.perseus/bundle.js"; async function main() {{ await init("{path_prefix}/.perseus/bundle.wasm"); - run(); }} main(); "#, @@ -92,10 +94,9 @@ impl HtmlShell { #[cfg(feature = "wasm2js")] let load_wasm_bundle = format!( r#" - import init, {{ main as run }} from "{path_prefix}/.perseus/bundle.js"; + import init from "{path_prefix}/.perseus/bundle.js"; async function main() {{ await init("{path_prefix}/.perseus/bundle.wasm.js"); - run(); }} main(); "#, diff --git a/scripts/example.rs b/scripts/example.rs index e9ba1d5b78..e9017ec4fe 100644 --- a/scripts/example.rs +++ b/scripts/example.rs @@ -20,11 +20,9 @@ fn main() { // These paths are for the CLI, which is inside `packages/perseus-cli` let cli_path = format!("../../examples/{}/{}", &category, &example); let cargo_args = format!("--features \"perseus-integration/{}\"", integration); - let envs = if integration_locked { - vec![("TEST_EXAMPLE", &cli_path)] - } else { - vec![("TEST_EXAMPLE", &cli_path), ("PERSEUS_CARGO_ARGS", &cargo_args)] - }; + if !integration_locked { + args.push(format!("--cargo-engine-args='{}'", &cargo_args)); + } #[cfg(unix)] let shell_exec = "sh"; @@ -39,7 +37,7 @@ fn main() { // We don't provide any quoted arguments to the CLI ever, so this is fine .args([shell_param, &format!("cargo run -- {}", args.join(" "))]) .current_dir("packages/perseus-cli") // We run on the bleeding-edge version of the CLI, from which we know the example from `TEST_EXAMPLE` above - .envs(envs) + .env("TEST_EXAMPLE", &cli_path) .spawn() .expect("couldn't run example (command execution failed)"); let _ = child.wait().expect("couldn't wait on example executor process");