feat(workout): create standalone app path
Add a workout new PATH command that copies the Workout exemplar into a standalone app with local hemx path dependencies, verifies its manifest in xtask tests, and documents the create/check path. req: examples/001 req: examples/006
This commit is contained in:
@@ -133,7 +133,8 @@ See `docs/versioning.md`.
|
|||||||
walkthrough in `docs/tutorial-saas.md`; the SQLx persistence recipe in
|
walkthrough in `docs/tutorial-saas.md`; the SQLx persistence recipe in
|
||||||
`docs/recipes/sqlx-persistence.md` shows the provider boundary without moving
|
`docs/recipes/sqlx-persistence.md` shows the provider boundary without moving
|
||||||
SQL into core.
|
SQL into core.
|
||||||
- `examples/workout`: phone-first local-first product exemplar. Run with
|
- `examples/workout`: phone-first local-first product exemplar. Create a starter
|
||||||
|
with `cargo run -p hemx-xtask -- workout new PATH`; run the exemplar with
|
||||||
`cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`.
|
`cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`.
|
||||||
It keeps workout truth as commands/events/projections and routes export
|
It keeps workout truth as commands/events/projections and routes export
|
||||||
through the host capability boundary; `examples/workout/README.md` documents
|
through the host capability boundary; `examples/workout/README.md` documents
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ final export shared or replayed, and invalid input, replay failure, or host deni
|
|||||||
recovered without storing DOM patches or UI update payloads as truth.
|
recovered without storing DOM patches or UI update payloads as truth.
|
||||||
req: examples/001 req: local/001 req: local/003 req: local/004
|
req: examples/001 req: local/001 req: local/003 req: local/004
|
||||||
|
|
||||||
|
## Create
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p hemx-xtask -- workout new /tmp/my-workout-app
|
||||||
|
cargo check --manifest-path /tmp/my-workout-app/Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
The created app is standalone: it points at local hemx crates, owns its
|
||||||
|
command/event/projection state, and keeps Android/iOS signing outside the repo.
|
||||||
|
req: examples/006
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+150
-4
@@ -9,7 +9,11 @@ fn main() -> ExitCode {
|
|||||||
match args.next().as_deref() {
|
match args.next().as_deref() {
|
||||||
Some("test") | None => run_test_plan(),
|
Some("test") | None => run_test_plan(),
|
||||||
Some("bench") => run_bench_plan(),
|
Some("bench") => run_bench_plan(),
|
||||||
Some("workout") => run_workout(args.next().as_deref()),
|
Some("workout") => {
|
||||||
|
let subcommand = args.next();
|
||||||
|
let operand = args.next();
|
||||||
|
run_workout(subcommand.as_deref(), operand.as_deref())
|
||||||
|
}
|
||||||
Some("workout-mobile") => run_workout_mobile(args.next().as_deref()),
|
Some("workout-mobile") => run_workout_mobile(args.next().as_deref()),
|
||||||
Some("help") | Some("--help") | Some("-h") => {
|
Some("help") | Some("--help") | Some("-h") => {
|
||||||
print_help();
|
print_help();
|
||||||
@@ -25,13 +29,14 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- bench\n cargo run -p hemx-xtask -- workout dev\n cargo run -p hemx-xtask -- workout test\n cargo run -p hemx-xtask -- workout build\n cargo run -p hemx-xtask -- workout mobile-release\n cargo run -p hemx-xtask -- workout mobile-verify\n cargo run -p hemx-xtask -- workout doctor\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E\n HEMX_WORKOUT_ORIGIN=https://app.example.com\n HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout"
|
"hemx-ci — resource-aware project checks\n\n cargo run -p hemx-xtask -- test\n cargo run -p hemx-xtask -- bench\n cargo run -p hemx-xtask -- workout new PATH\n cargo run -p hemx-xtask -- workout dev\n cargo run -p hemx-xtask -- workout test\n cargo run -p hemx-xtask -- workout build\n cargo run -p hemx-xtask -- workout mobile-release\n cargo run -p hemx-xtask -- workout mobile-verify\n cargo run -p hemx-xtask -- workout doctor\n\nEnvironment overrides:\n HEMX_CI_JOBS=N compile jobs, capped by detected resources\n HEMX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n HEMX_CI_SKIP_BROWSER=1 skip browser E2E\n HEMX_WORKOUT_ORIGIN=https://app.example.com\n HEMX_WORKOUT_MOBILE_OUT=target/hemx-mobile/workout"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_workout(command: Option<&str>) -> ExitCode {
|
fn run_workout(command: Option<&str>, operand: Option<&str>) -> ExitCode {
|
||||||
// req: examples/001 req: examples/006
|
// req: examples/001 req: examples/006
|
||||||
match command.unwrap_or("dev") {
|
match command.unwrap_or("dev") {
|
||||||
|
"new" | "create" => run_workout_new(operand),
|
||||||
"dev" | "run" => Step::new(
|
"dev" | "run" => Step::new(
|
||||||
"workout-dev-server",
|
"workout-dev-server",
|
||||||
["run", "--bin", "hemx-workout-example"],
|
["run", "--bin", "hemx-workout-example"],
|
||||||
@@ -64,6 +69,126 @@ fn run_workout(command: Option<&str>) -> ExitCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_workout_new(destination: Option<&str>) -> ExitCode {
|
||||||
|
// req: examples/001 req: examples/006
|
||||||
|
let Some(destination) = destination else {
|
||||||
|
eprintln!("usage: cargo run -p hemx-xtask -- workout new PATH");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
};
|
||||||
|
let destination = PathBuf::from(destination);
|
||||||
|
if destination.exists() {
|
||||||
|
eprintln!(
|
||||||
|
"{} already exists; choose an empty path",
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
match create_workout_app(&destination) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("workout-new\tpath={}", destination.display());
|
||||||
|
println!(
|
||||||
|
"next\tcargo run --manifest-path {}/Cargo.toml --bin workout-app",
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"failed to create Workout app at {}: {err}",
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_workout_app(destination: &Path) -> std::io::Result<()> {
|
||||||
|
let root = repo_root()?;
|
||||||
|
copy_dir(&root.join("examples/workout"), destination)?;
|
||||||
|
let cargo_toml = destination.join("Cargo.toml");
|
||||||
|
let manifest = fs::read_to_string(&cargo_toml)?;
|
||||||
|
fs::write(
|
||||||
|
&cargo_toml,
|
||||||
|
manifest
|
||||||
|
.replace(
|
||||||
|
"name = \"hemx-workout-example\"",
|
||||||
|
"name = \"hemx-workout-app\"",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"[[bin]]\nname = \"hemx-workout-app\"",
|
||||||
|
"[[bin]]\nname = \"workout-app\"",
|
||||||
|
)
|
||||||
|
.replace("version.workspace = true", "version = \"0.1.0\"")
|
||||||
|
.replace("edition.workspace = true", "edition = \"2021\"")
|
||||||
|
.replace(
|
||||||
|
"path = \"../../../hemplate/hemplate\"",
|
||||||
|
&format!("path = \"{}\"", root.join("../hemplate/hemplate").display()),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"path = \"../../hemx\"",
|
||||||
|
&format!("path = \"{}\"", root.join("hemx").display()),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"path = \"../../hemx-axum\"",
|
||||||
|
&format!("path = \"{}\"", root.join("hemx-axum").display()),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"path = \"../../hemx-host\"",
|
||||||
|
&format!("path = \"{}\"", root.join("hemx-host").display()),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"path = \"../../hemx-test\"",
|
||||||
|
&format!("path = \"{}\"", root.join("hemx-test").display()),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"path = \"../../hemx-build\"",
|
||||||
|
&format!("path = \"{}\"", root.join("hemx-build").display()),
|
||||||
|
)
|
||||||
|
.replace("hemx_workout_example", "hemx_workout_app"),
|
||||||
|
)?;
|
||||||
|
replace_in_file(
|
||||||
|
&destination.join("src/main.rs"),
|
||||||
|
"hemx_workout_example",
|
||||||
|
"hemx_workout_app",
|
||||||
|
)?;
|
||||||
|
fs::write(
|
||||||
|
destination.join("CREATED.md"),
|
||||||
|
"# Created Workout app\n\nRun locally with:\n\n```sh\ncargo run --manifest-path Cargo.toml --bin workout-app\n```\n\nBuild for production with:\n\n```sh\ncargo build --manifest-path Cargo.toml --release --bin workout-app\n```\n\nThis app owns command/event/projection state and keeps Android/iOS signing outside the repo. req: examples/006\n",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo_root() -> std::io::Result<PathBuf> {
|
||||||
|
let cwd = env::current_dir()?;
|
||||||
|
if cwd.join("examples/workout").exists() {
|
||||||
|
return Ok(cwd);
|
||||||
|
}
|
||||||
|
Ok(PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_in_file(path: &Path, from: &str, to: &str) -> std::io::Result<()> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
fs::write(path, contents.replace(from, to))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dir(source: &Path, destination: &Path) -> std::io::Result<()> {
|
||||||
|
fs::create_dir_all(destination)?;
|
||||||
|
for entry in fs::read_dir(source)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let source_path = entry.path();
|
||||||
|
let destination_path = destination.join(entry.file_name());
|
||||||
|
if source_path.is_dir() {
|
||||||
|
copy_dir(&source_path, &destination_path)?;
|
||||||
|
} else {
|
||||||
|
fs::copy(&source_path, &destination_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_workout_mobile(command: Option<&str>) -> ExitCode {
|
fn run_workout_mobile(command: Option<&str>) -> ExitCode {
|
||||||
match command.unwrap_or("release") {
|
match command.unwrap_or("release") {
|
||||||
"release" => run_workout_mobile_release(),
|
"release" => run_workout_mobile_release(),
|
||||||
@@ -744,7 +869,7 @@ fn is_executable(path: impl AsRef<Path>) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
android_twa_release_json, mobile_external_blockers, origin_host,
|
android_twa_release_json, create_workout_app, mobile_external_blockers, origin_host,
|
||||||
verify_workout_mobile_release, workout_mobile_manifest, write_workout_mobile_release,
|
verify_workout_mobile_release, workout_mobile_manifest, write_workout_mobile_release,
|
||||||
Budget, WorkoutMobileConfig,
|
Budget, WorkoutMobileConfig,
|
||||||
};
|
};
|
||||||
@@ -782,6 +907,27 @@ mod tests {
|
|||||||
assert_eq!(super::bench_values(6), vec![1, 2, 4, 6]);
|
assert_eq!(super::bench_values(6), vec![1, 2, 4, 6]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workout_new_creates_standalone_app_manifest() {
|
||||||
|
// req: examples/001 req: examples/006
|
||||||
|
let destination = PathBuf::from("target/test-workout-new-app");
|
||||||
|
let _ = fs::remove_dir_all(&destination);
|
||||||
|
|
||||||
|
create_workout_app(&destination).expect("create app");
|
||||||
|
let manifest = fs::read_to_string(destination.join("Cargo.toml")).expect("manifest");
|
||||||
|
|
||||||
|
assert!(manifest.contains("name = \"hemx-workout-app\""));
|
||||||
|
assert!(manifest.contains("name = \"workout-app\""));
|
||||||
|
assert!(manifest.contains("edition = \"2021\""));
|
||||||
|
assert!(manifest.contains("hemx-build"));
|
||||||
|
let main_rs = fs::read_to_string(destination.join("src/main.rs")).expect("main rs");
|
||||||
|
assert!(main_rs.contains("hemx_workout_app"));
|
||||||
|
assert!(!main_rs.contains("hemx_workout_example"));
|
||||||
|
assert!(destination.join("src/lib.rs").exists());
|
||||||
|
assert!(destination.join("templates/workout.heml").exists());
|
||||||
|
let _ = fs::remove_dir_all(&destination);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workout_mobile_manifest_names_production_boundaries() {
|
fn workout_mobile_manifest_names_production_boundaries() {
|
||||||
// req: examples/001 req: local/001 req: host/002
|
// req: examples/001 req: local/001 req: host/002
|
||||||
|
|||||||
Reference in New Issue
Block a user