Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "star-setup"
version = "0.4.6"
version = "0.4.7"
edition = "2021"
repository = "https://github.com/star-setup/core"
description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems"
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ Interactive mode complete
### Single Repository Mode
Build system is auto-detected from the repository root (`CMakeLists.txt` → CMake, `meson.build` → Meson, `package.json` → npm). Pass `--dev` to launch the project's dev server after setup.

Endpoint-style npm projects (no `build` script) skip the build step. If there's also no `dev` script, `--dev` falls back to `vercel dev` when the repo looks like a Vercel project (`vercel.json`, an `api/` directory, or `@vercel/node` as a dependency).

```bash
# Clone and build using a single repository
star-setup username/repo
Expand Down Expand Up @@ -220,10 +222,11 @@ star-setup username/repo --repos user/lib1 user/lib2
star-setup username/repo --repos user/lib1 user/lib2 --no-watch
```

After setup pass `--dev` to launch it automatically, or run the game from its repo directory:
After setup pass `--dev` to launch it automatically: falls back to `vercel dev` if the test repo has no `dev` script but looks like a Vercel project. Or, run it manually from its repo directory:
```bash
cd build-mono/repos/user-my-repo
npm run dev
# npm run dev
# vercel dev
```

</details>
Expand Down
21 changes: 14 additions & 7 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
cli::{BuildSystem, ResolvedArgs},
commands::read_package_json,
ctx::RunCtx,
};
use std::path::Path;
Expand Down Expand Up @@ -103,13 +104,19 @@ pub fn npm_build(
writeln!(ctx.io.output, "Installing dependencies").ok();
run_timed(&["npm", "install"], Some(source_path), "npm install", ctx)?;
if !args.build.no_build && !is_mono {
writeln!(ctx.io.output, "Building project").ok();
run_timed(
&["npm", "run", "build"],
Some(source_path),
"npm build",
ctx,
)?;
let has_build = read_package_json(source_path, "skipping build", &mut ctx.io, ctx.flags)
.is_some_and(|j| j.get("scripts").and_then(|s| s.get("build")).is_some());
if has_build {
writeln!(ctx.io.output, "Building project").ok();
run_timed(
&["npm", "run", "build"],
Some(source_path),
"npm build",
ctx,
)?;
} else if ctx.flags.verbose {
writeln!(ctx.io.output, " No build script found, skipping build").ok();
}
}
Ok(())
}
Expand Down
23 changes: 17 additions & 6 deletions src/commands/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
commands::read_package_json,
ctx::{IoCtx, RunCtx, RunFlags},
};
use serde_json::Value;
use std::{
path::Path,
process::{Command, Stdio},
Expand All @@ -21,13 +22,23 @@ pub fn resolve_dev_command(
) -> Option<String> {
let json = read_package_json(repo_path, "skipping dev server", io, flags)?;
if json.get("scripts").and_then(|s| s.get("dev")).is_some() {
Some("npm run dev".to_string())
} else {
if flags.verbose {
writeln!(io.output, " No dev script found, skipping dev server").ok();
}
None
return Some("npm run dev".to_string());
}
if is_vercel_project(repo_path, &json) {
return Some("vercel dev".to_string());
}
if flags.verbose {
writeln!(io.output, " No dev script found, skipping dev server").ok();
}
None
}

fn is_vercel_project(repo_path: &Path, json: &Value) -> bool {
repo_path.join("vercel.json").exists()
|| repo_path.join("api").is_dir()
|| ["dependencies", "devDependencies"]
.iter()
.any(|k| json.get(k).and_then(|d| d.get("@vercel/node")).is_some())
}

/// Runs the dev server in the foreground, blocking until it exits (Ctrl-C).
Expand Down
2 changes: 2 additions & 0 deletions tests/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod common;

#[path = "commands/build.rs"]
mod build;
#[path = "commands/dev.rs"]
mod dev;
#[path = "commands/display.rs"]
mod display;
#[path = "commands/header.rs"]
Expand Down
29 changes: 29 additions & 0 deletions tests/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use star_setup::{
cli::BuildSystem,
commands::{build_project, cmake_build, meson_build, npm_build},
};
use std::fs::write;

/* ===== BUILD_PROJECT ===== */
#[test]
Expand Down Expand Up @@ -99,6 +100,11 @@ fn test_npm_build_install_only() {
fn test_npm_build_with_build_step() {
let args = default_resolved_with_no_build(false);
let runner = with_ctx_runner(MockRunner::new(), |tmp_path, ctx| {
write(
tmp_path.join("package.json"),
r#"{"scripts": {"build": "tsc"}}"#,
)
.unwrap();
npm_build(&args, tmp_path, false, ctx).unwrap();
});
assert_eq!(runner.calls.len(), 2);
Expand All @@ -114,3 +120,26 @@ fn test_npm_build_install_only_prints_header() {
let out = String::from_utf8(output).unwrap();
assert!(out.contains("Installing dependencies"));
}

#[test]
fn test_npm_build_skips_build_without_script() {
let args = default_resolved_with_no_build(false);
let runner = with_ctx_runner(MockRunner::new(), |tmp_path, ctx| {
write(
tmp_path.join("package.json"),
r#"{"scripts": {"typecheck": "tsc --noEmit"}}"#,
)
.unwrap();
npm_build(&args, tmp_path, false, ctx).unwrap();
});
assert_eq!(runner.calls.len(), 1);
}

#[test]
fn test_npm_build_skips_build_when_package_json_missing() {
let args = default_resolved_with_no_build(false);
let runner = with_ctx_runner(MockRunner::new(), |tmp_path, ctx| {
npm_build(&args, tmp_path, false, ctx).unwrap();
});
assert_eq!(runner.calls.len(), 1);
}
79 changes: 79 additions & 0 deletions tests/commands/dev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::common::with_io_output;
use star_setup::{commands::resolve_dev_command, ctx::RunFlags};
use std::fs::{create_dir_all, write};
use tempfile::TempDir;

fn flags(verbose: bool) -> RunFlags {
RunFlags {
verbose,
timing: false,
dry_run: false,
}
}

#[test]
fn test_resolve_dev_command_uses_dev_script() {
let tmp = TempDir::new().unwrap();
write(
tmp.path().join("package.json"),
r#"{"scripts": {"dev": "vite"}}"#,
)
.unwrap();
let (result, _) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(false)));
assert_eq!(result, Some("npm run dev".to_string()));
}

#[test]
fn test_resolve_dev_command_falls_back_to_vercel_dev_with_vercel_json() {
let tmp = TempDir::new().unwrap();
write(tmp.path().join("package.json"), r#"{"scripts": {}}"#).unwrap();
write(tmp.path().join("vercel.json"), "{}").unwrap();
let (result, _) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(false)));
assert_eq!(result, Some("vercel dev".to_string()));
}

#[test]
fn test_resolve_dev_command_falls_back_to_vercel_dev_with_api_dir() {
let tmp = TempDir::new().unwrap();
write(tmp.path().join("package.json"), r#"{"scripts": {}}"#).unwrap();
create_dir_all(tmp.path().join("api")).unwrap();
let (result, _) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(false)));
assert_eq!(result, Some("vercel dev".to_string()));
}

#[test]
fn test_resolve_dev_command_falls_back_to_vercel_dev_with_vercel_node_dependency() {
let tmp = TempDir::new().unwrap();
write(
tmp.path().join("package.json"),
r#"{"scripts": {}, "dependencies": {"@vercel/node": "^5.0.0"}}"#,
)
.unwrap();
let (result, _) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(false)));
assert_eq!(result, Some("vercel dev".to_string()));
}

#[test]
fn test_resolve_dev_command_falls_back_to_vercel_dev_with_vercel_node_dev_dependency() {
let tmp = TempDir::new().unwrap();
write(
tmp.path().join("package.json"),
r#"{"scripts": {}, "devDependencies": {"@vercel/node": "^5.0.0"}}"#,
)
.unwrap();
let (result, _) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(false)));
assert_eq!(result, Some("vercel dev".to_string()));
}

#[test]
fn test_resolve_dev_command_none_when_no_signals() {
let tmp = TempDir::new().unwrap();
write(
tmp.path().join("package.json"),
r#"{"scripts": {"typecheck": "tsc"}}"#,
)
.unwrap();
let (result, out) = with_io_output(|io| resolve_dev_command(tmp.path(), io, flags(true)));
assert!(result.is_none());
assert!(out.contains("No dev script found"));
}
Loading