use anyhow::{Context, Result};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::build::run_build;
use crate::cli::{BuildArgs, BuildProjectArgs};
use crate::rolldown_bundler;
use crate::swc_compiler::generate_declarations;
use crate::utils::copy_dir_recursive;
use crate::web_bundler::{self, WebBundleConfig};
struct WorkspaceDepInfo {
name: String,
built_path: PathBuf,
source_path: Option<PathBuf>,
}
pub fn run_build_project(args: BuildProjectArgs) -> Result<()> {
println!("pleme-linker build-project: Building TypeScript project");
println!(" Project: {}", args.project.display());
println!(" Output: {}", args.output.display());
println!(" Manifest: {}", args.manifest.display());
println!();
let lib_dir = args.output.join("lib");
let bin_dir = args.output.join("bin");
fs::create_dir_all(&lib_dir)?;
fs::create_dir_all(&bin_dir)?;
println!("Stage 1: Building node_modules...");
let node_modules_dir = lib_dir.join("node_modules_build");
run_build(BuildArgs {
manifest: args.manifest.clone(),
output: node_modules_dir.clone(),
node_bin: args.node_bin.clone(),
})?;
let mut built_workspace_deps: Vec<WorkspaceDepInfo> = args
.workspace_dep
.iter()
.map(|(name, path)| WorkspaceDepInfo {
name: name.clone(),
built_path: path.clone(),
source_path: None,
})
.collect();
if !args.workspace_src.is_empty() {
println!();
println!("Stage 1.5: Building workspace packages from source...");
let workspace_build_root = lib_dir.join("workspace_builds");
fs::create_dir_all(&workspace_build_root)?;
for ws in &args.workspace_src {
println!(" Building workspace package: {}", ws.name);
let safe_name = ws.name.replace('@', "").replace('/', "-");
let ws_output = workspace_build_root.join(&safe_name);
fs::create_dir_all(&ws_output)?;
let deps_for_recursive: Vec<(String, PathBuf)> = built_workspace_deps
.iter()
.map(|d| (d.name.clone(), d.built_path.clone()))
.collect();
build_workspace_package(
&ws.name,
&ws.manifest,
&ws.src,
&ws_output,
&args.node_bin,
args.parent_tsconfig.as_deref(),
&deps_for_recursive,
)?;
built_workspace_deps.push(WorkspaceDepInfo {
name: ws.name.clone(),
built_path: ws_output.join("lib"),
source_path: Some(ws.src.clone()),
});
}
}
println!();
println!("Stage 2: Setting up build environment...");
let build_dir = lib_dir.join("build_workspace");
fs::create_dir_all(&build_dir)?;
copy_dir_recursive(&args.project, &build_dir)?;
let build_node_modules = build_dir.join("node_modules");
if build_node_modules.exists() {
fs::remove_dir_all(&build_node_modules)?;
}
std::os::unix::fs::symlink(node_modules_dir.join("node_modules"), &build_node_modules)?;
if let Some(parent_tsconfig) = &args.parent_tsconfig {
let parent_dest = build_dir.parent().unwrap().join("tsconfig.json");
fs::copy(parent_tsconfig, &parent_dest)?;
}
for dep_info in &built_workspace_deps {
setup_workspace_dep(&build_dir, &build_node_modules, dep_info)?;
}
println!();
let index_html = build_dir.join("index.html");
let src_dir = build_dir.join("src");
let main_tsx = src_dir.join("main.tsx");
let index_tsx = src_dir.join("index.tsx");
let is_web_app = index_html.exists() && (main_tsx.exists() || index_tsx.exists());
let dist_dir = build_dir.join("dist");
fs::create_dir_all(&dist_dir)?;
if is_web_app {
if args.use_vite {
println!("Stage 3: Building web application with Vite...");
let status = Command::new(&args.node_bin)
.args(["--", &build_node_modules.join(".bin/vite").to_string_lossy(), "build"])
.current_dir(&build_dir)
.env("NODE_ENV", "production")
.status()
.with_context(|| "Failed to run Vite build")?;
if !status.success() {
anyhow::bail!("Vite build failed with exit code: {:?}", status.code());
}
println!(" Vite build successful!");
} else {
println!("Stage 3: Building web application with OXC bundler...");
let entry_point = if main_tsx.exists() { main_tsx } else { index_tsx };
let public_dir = build_dir.join("public");
let config = WebBundleConfig {
project_root: build_dir.clone(),
src_dir: src_dir.clone(),
out_dir: dist_dir.clone(),
index_html: index_html.clone(),
entry_point,
public_dir: if public_dir.exists() { Some(public_dir) } else { None },
base_path: "/".to_string(),
minify: true,
externals: web_bundler::default_react_externals(),
bundle_node_modules: false,
};
let result = web_bundler::bundle_web_app(&config)
.with_context(|| "Web bundler failed")?;
println!(" Bundle: {}", result.js_bundle.file_name().unwrap().to_string_lossy());
println!(" Hash: {}", result.bundle_hash);
println!(" Assets: {} files", result.assets.len());
println!(" OXC build successful!");
}
} else {
println!("Stage 3: Compiling TypeScript with OXC (pure Rust)...");
let compiled = rolldown_bundler::compile_typescript(&src_dir, &dist_dir)
.with_context(|| "OXC TypeScript compilation failed")?;
println!(" Compiled {} files with OXC", compiled.len());
let declarations = generate_declarations(&src_dir, &dist_dir)
.with_context(|| "Declaration file generation failed")?;
println!(" Generated {} declaration files", declarations.len());
println!(" TypeScript compilation successful");
}
println!();
println!("Stage 4: Copying build output...");
let dist_dir = build_dir.join("dist");
if !dist_dir.exists() {
anyhow::bail!("dist/ directory not found after compilation");
}
let output_dist = lib_dir.join("dist");
copy_dir_recursive(&dist_dir, &output_dist)?;
let output_node_modules = lib_dir.join("node_modules");
std::os::unix::fs::symlink(node_modules_dir.join("node_modules"), &output_node_modules)?;
if let (Some(cli_entry), Some(bin_name)) = (&args.cli_entry, &args.bin_name) {
println!();
println!("Stage 5: Creating wrapper script...");
let cli_path = output_dist.join(cli_entry);
if !cli_path.exists() {
anyhow::bail!("CLI entry point not found: {}", cli_path.display());
}
let wrapper_path = bin_dir.join(bin_name);
let wrapper_content = format!(
"#!{}\nexec {} {} \"$@\"\n",
"/usr/bin/env bash",
args.node_bin.display(),
cli_path.display()
);
fs::write(&wrapper_path, wrapper_content)?;
fs::set_permissions(&wrapper_path, fs::Permissions::from_mode(0o755))?;
println!(" Created: {}", wrapper_path.display());
}
println!();
println!("Stage 6: Cleaning up...");
fs::remove_dir_all(&build_dir)?;
println!();
println!("Done!");
println!(" Output: {}", args.output.display());
Ok(())
}
fn setup_workspace_dep(
build_dir: &Path,
build_node_modules: &Path,
dep_info: &WorkspaceDepInfo,
) -> Result<()> {
println!(
" Setting up workspace dep: {} -> {}",
dep_info.name,
dep_info.built_path.display()
);
let dep_target = if dep_info.name.starts_with('@') {
let parts: Vec<&str> = dep_info.name.splitn(2, '/').collect();
if parts.len() == 2 {
let scope_dir = build_node_modules.join(parts[0]);
fs::create_dir_all(&scope_dir)?;
scope_dir.join(parts[1])
} else {
build_node_modules.join(&dep_info.name)
}
} else {
build_node_modules.join(&dep_info.name)
};
if dep_target.symlink_metadata().is_ok() {
fs::remove_file(&dep_target).or_else(|_| fs::remove_dir_all(&dep_target))?;
}
std::os::unix::fs::symlink(&dep_info.built_path, &dep_target)?;
let sibling_name = dep_info
.name
.split('/')
.last()
.unwrap_or(&dep_info.name);
let sibling_dir = build_dir.parent().unwrap().join(sibling_name);
if !sibling_dir.exists() {
fs::create_dir_all(&sibling_dir)?;
if dep_info.built_path.join("dist").exists() {
copy_dir_recursive(&dep_info.built_path.join("dist"), &sibling_dir.join("dist"))?;
}
if dep_info.built_path.join("package.json").exists() {
fs::copy(
dep_info.built_path.join("package.json"),
sibling_dir.join("package.json"),
)?;
}
if let Some(source_path) = &dep_info.source_path {
let tsconfig_src = source_path.join("tsconfig.json");
if tsconfig_src.exists() {
fs::copy(&tsconfig_src, sibling_dir.join("tsconfig.json"))?;
}
}
}
Ok(())
}
fn build_workspace_package(
name: &str,
manifest: &Path,
src: &Path,
output: &Path,
node_bin: &Path,
parent_tsconfig: Option<&Path>,
existing_workspace_deps: &[(String, PathBuf)],
) -> Result<()> {
println!(" Building workspace package: {}", name);
let lib_dir = output.join("lib");
fs::create_dir_all(&lib_dir)?;
let node_modules_dir = lib_dir.join("node_modules_build");
run_build(BuildArgs {
manifest: manifest.to_path_buf(),
output: node_modules_dir.clone(),
node_bin: node_bin.to_path_buf(),
})?;
let build_dir = lib_dir.join("build_workspace");
fs::create_dir_all(&build_dir)?;
copy_dir_recursive(src, &build_dir)?;
let build_node_modules = build_dir.join("node_modules");
if build_node_modules.exists() {
fs::remove_dir_all(&build_node_modules)?;
}
std::os::unix::fs::symlink(node_modules_dir.join("node_modules"), &build_node_modules)?;
if let Some(parent_tsconfig) = parent_tsconfig {
let parent_dest = build_dir.parent().unwrap().join("tsconfig.json");
fs::copy(parent_tsconfig, &parent_dest)?;
}
for (dep_name, dep_path) in existing_workspace_deps {
let dep_target = if dep_name.starts_with('@') {
let parts: Vec<&str> = dep_name.splitn(2, '/').collect();
if parts.len() == 2 {
let scope_dir = build_node_modules.join(parts[0]);
fs::create_dir_all(&scope_dir)?;
scope_dir.join(parts[1])
} else {
build_node_modules.join(dep_name)
}
} else {
build_node_modules.join(dep_name)
};
if dep_target.symlink_metadata().is_ok() {
fs::remove_file(&dep_target).or_else(|_| fs::remove_dir_all(&dep_target))?;
}
std::os::unix::fs::symlink(dep_path, &dep_target)?;
}
let ws_src_dir = build_dir.join("src");
let ws_dist_dir = build_dir.join("dist");
fs::create_dir_all(&ws_dist_dir)?;
rolldown_bundler::compile_typescript(&ws_src_dir, &ws_dist_dir)
.with_context(|| format!("OXC compilation failed for workspace package {}", name))?;
generate_declarations(&ws_src_dir, &ws_dist_dir)
.with_context(|| format!("Declaration generation failed for workspace package {}", name))?;
let dist_dir = build_dir.join("dist");
if !dist_dir.exists() {
anyhow::bail!(
"dist/ directory not found after compilation for workspace package {}",
name
);
}
let output_dist = lib_dir.join("dist");
copy_dir_recursive(&dist_dir, &output_dist)?;
let package_json_src = src.join("package.json");
if package_json_src.exists() {
fs::copy(&package_json_src, lib_dir.join("package.json"))?;
}
let output_node_modules = lib_dir.join("node_modules");
std::os::unix::fs::symlink(node_modules_dir.join("node_modules"), &output_node_modules)?;
fs::remove_dir_all(&build_dir)?;
println!(" Workspace package {} built successfully", name);
Ok(())
}