use anyhow::{Context, Result};
use clap::Parser;
use rustc_hash::FxHashMap;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::time::Duration;
use tsz::checker::diagnostics::DiagnosticCategory;
use tsz_cli::args::CliArgs;
use tsz_cli::{driver, locale, reporter::Reporter, watch};
const EXIT_SUCCESS: i32 = 0;
const EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED: i32 = 1;
const EXIT_DIAGNOSTICS_OUTPUTS_GENERATED: i32 = 2;
fn main() -> Result<()> {
tsz_cli::tracing_config::init_tracing();
let preprocessed = preprocess_args(std::env::args_os().collect());
let args = CliArgs::parse_from(preprocessed);
let cwd = std::env::current_dir().context("failed to resolve current directory")?;
if should_use_large_stack_thread(&args) {
const MAIN_STACK_SIZE: usize = 64 * 1024 * 1024;
std::thread::Builder::new()
.stack_size(MAIN_STACK_SIZE)
.spawn(move || actual_main(args, cwd))
.expect("failed to spawn main thread")
.join()
.expect("main thread panicked")
} else {
actual_main(args, cwd)
}
}
fn actual_main(args: CliArgs, cwd: std::path::PathBuf) -> Result<()> {
locale::init_locale(args.locale.as_deref());
if args.batch {
return run_batch_mode();
}
if args.init {
return handle_init(&args, &cwd);
}
if args.show_config {
return handle_show_config(&args, &cwd);
}
if args.list_files_only {
return handle_list_files_only(&args, &cwd);
}
if args.all {
return handle_all();
}
if args.build {
return handle_build(&args, &cwd);
}
if args.watch {
return watch::run(&args, &cwd);
}
let tracer = args.generate_trace.is_some().then(|| {
let mut t = tsz_cli::trace::Tracer::new();
let mut meta_args = FxHashMap::default();
meta_args.insert("name".to_string(), serde_json::json!("tsz"));
t.metadata("process_name", meta_args);
t
});
if let Some(ref _profile_path) = args.generate_cpu_profile {
println!(
"The --generateCpuProfile flag is a V8/Node.js feature and is not applicable to tsz (a native Rust compiler). The flag is accepted for compatibility but has no effect."
);
}
let start_time = std::time::Instant::now();
let result = driver::compile(&args, &cwd)?;
let elapsed = start_time.elapsed();
if let (Some(trace_path), Some(mut tracer)) = (args.generate_trace.as_ref(), tracer) {
use tsz_cli::trace::categories;
tracer.complete_with_args("Compile", categories::PROGRAM, start_time, elapsed, {
let mut args = FxHashMap::default();
args.insert(
"fileCount".to_string(),
serde_json::json!(result.files_read.len()),
);
args.insert(
"errorCount".to_string(),
serde_json::json!(result.diagnostics.len()),
);
args.insert(
"emittedCount".to_string(),
serde_json::json!(result.emitted_files.len()),
);
args
});
for file in &result.files_read {
let mut args = FxHashMap::default();
args.insert(
"path".to_string(),
serde_json::json!(file.display().to_string()),
);
tracer.instant_with_args("FileProcessed", categories::IO, args);
}
let trace_file = if trace_path.is_dir() {
trace_path.join("trace.json")
} else {
trace_path.to_path_buf()
};
if let Err(e) = tracer.write_to_file(&trace_file) {
println!("Warning: Failed to write trace file: {e}");
} else {
println!("Trace written to: {}", trace_file.display());
}
}
if args.list_files {
for file in &result.files_read {
println!("{}", file.display());
}
}
if args.list_emitted_files && !result.emitted_files.is_empty() {
for file in &result.emitted_files {
println!("TSFILE: {}", file.display());
}
}
if args.explain_files {
for info in &result.file_infos {
println!("{}", info.path.display());
for reason in &info.reasons {
println!(" {reason}");
}
}
}
if args.trace_dependencies {
for file in &result.files_read {
println!("{}", file.display());
}
}
if args.diagnostics || args.extended_diagnostics {
print_diagnostics(&result, elapsed, args.extended_diagnostics);
}
if !result.diagnostics.is_empty() {
let pretty = args
.pretty
.unwrap_or_else(|| std::io::stderr().is_terminal());
let mut reporter = Reporter::new(pretty);
let output = reporter.render(&result.diagnostics);
if !output.is_empty() {
print!("{output}");
}
}
let has_errors = result
.diagnostics
.iter()
.any(|diag| diag.category == DiagnosticCategory::Error);
if has_errors {
if args.no_emit || !result.emitted_files.is_empty() {
std::process::exit(EXIT_DIAGNOSTICS_OUTPUTS_GENERATED);
} else {
std::process::exit(EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED);
}
}
std::process::exit(EXIT_SUCCESS);
}
const fn should_use_large_stack_thread(args: &CliArgs) -> bool {
args.project.is_some() || args.build || args.watch || args.batch || args.files.len() != 1
}
fn run_batch_mode() -> Result<()> {
use std::io::{BufRead, Write};
let stdin = std::io::stdin();
let reader = stdin.lock();
let mut stdout = std::io::stdout().lock();
for line in reader.lines() {
let line = line.context("failed to read from stdin")?;
let project_dir = line.trim();
if project_dir.is_empty() {
writeln!(stdout, "---TSZ-BATCH-DONE---")?;
stdout.flush()?;
continue;
}
let project_path = std::path::Path::new(project_dir);
let batch_args = CliArgs::parse_from([
"tsz",
"--project",
project_dir,
"--noEmit",
"--pretty",
"false",
]);
match driver::compile(&batch_args, project_path) {
Ok(result) => {
if !result.diagnostics.is_empty() {
let mut reporter = Reporter::new(false);
let output = reporter.render(&result.diagnostics);
if !output.is_empty() {
write!(stdout, "{output}")?;
}
}
}
Err(e) => {
writeln!(stdout, "error: {e}")?;
}
}
writeln!(stdout, "---TSZ-BATCH-DONE---")?;
stdout.flush()?;
}
Ok(())
}
fn preprocess_args(args: Vec<OsString>) -> Vec<OsString> {
let mut result = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
let arg_str = arg.to_string_lossy();
if i == 0 {
result.push(arg.clone());
continue;
}
if arg_str == "-v" {
result.push(OsString::from("-V"));
} else if arg_str.starts_with('@') && arg_str.len() > 1 {
let path = &arg_str[1..];
match std::fs::read_to_string(path) {
Ok(content) => {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
for part in split_response_line(trimmed) {
result.push(OsString::from(part));
}
}
}
}
Err(_) => {
result.push(arg.clone());
}
}
} else {
result.push(arg.clone());
}
}
result
}
fn split_response_line(line: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_quote: Option<char> = None;
for ch in line.chars() {
match in_quote {
Some(q) if ch == q => {
in_quote = None;
}
Some(_) => {
current.push(ch);
}
None if ch == '"' || ch == '\'' => {
in_quote = Some(ch);
}
None if ch.is_ascii_whitespace() => {
if !current.is_empty() {
args.push(std::mem::take(&mut current));
}
}
None => {
current.push(ch);
}
}
}
if !current.is_empty() {
args.push(current);
}
args
}
fn print_diagnostics(result: &driver::CompilationResult, elapsed: Duration, extended: bool) {
let files_count = result.files_read.len();
let mut lines_of_library: u64 = 0;
let mut lines_of_definitions: u64 = 0;
let mut lines_of_typescript: u64 = 0;
let mut lines_of_javascript: u64 = 0;
let mut lines_of_json: u64 = 0;
let mut lines_of_other: u64 = 0;
for path in &result.files_read {
let count = std::fs::read_to_string(path)
.ok()
.map_or(0, |text| text.lines().count() as u64);
let name = path.to_string_lossy();
if name.contains("lib.") && name.ends_with(".d.ts") {
lines_of_library += count;
} else if name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts") {
lines_of_definitions += count;
} else if name.ends_with(".ts")
|| name.ends_with(".tsx")
|| name.ends_with(".mts")
|| name.ends_with(".cts")
{
lines_of_typescript += count;
} else if name.ends_with(".js")
|| name.ends_with(".jsx")
|| name.ends_with(".mjs")
|| name.ends_with(".cjs")
{
lines_of_javascript += count;
} else if name.ends_with(".json") {
lines_of_json += count;
} else {
lines_of_other += count;
}
}
let errors = result
.diagnostics
.iter()
.filter(|d| d.category == DiagnosticCategory::Error)
.count();
println!();
println!("Files: {files_count}");
println!("Lines of Library: {lines_of_library}");
println!("Lines of Definitions: {lines_of_definitions}");
println!("Lines of TypeScript: {lines_of_typescript}");
println!("Lines of JavaScript: {lines_of_javascript}");
println!("Lines of JSON: {lines_of_json}");
println!("Lines of Other: {lines_of_other}");
println!("Errors: {errors}");
println!(
"Total time: {:.2}s",
elapsed.as_secs_f64()
);
if extended {
let memory_used = get_memory_usage_kb();
println!(
"Emitted files: {}",
result.emitted_files.len()
);
println!(
"Total diagnostics: {}",
result.diagnostics.len()
);
if memory_used > 0 {
println!("Memory used: {memory_used}K");
}
}
}
fn get_memory_usage_kb() -> u64 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|status| {
for line in status.lines() {
if line.starts_with("VmRSS:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
return parts[1].parse::<u64>().ok();
}
}
}
None
})
.unwrap_or(0)
}
fn handle_init(_args: &CliArgs, cwd: &std::path::Path) -> Result<()> {
let tsconfig_path = cwd.join("tsconfig.json");
if tsconfig_path.exists() {
println!(
"A tsconfig.json file is already defined at: {}",
tsconfig_path.display()
);
std::process::exit(1);
}
let config = r#"{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}
"#;
std::fs::write(&tsconfig_path, config).with_context(|| {
format!(
"failed to write tsconfig.json to {}",
tsconfig_path.display()
)
})?;
println!(
"\nCreated a new tsconfig.json\n\n\
You can learn more at https://aka.ms/tsconfig"
);
Ok(())
}
fn handle_show_config(args: &CliArgs, cwd: &std::path::Path) -> Result<()> {
use tsz_cli::config::{load_tsconfig, resolve_compiler_options};
use tsz_cli::driver::apply_cli_overrides;
let tsconfig_path = args
.project
.as_ref()
.map(|p| {
if p.is_dir() {
p.join("tsconfig.json")
} else {
p.clone()
}
})
.or_else(|| {
let default_path = cwd.join("tsconfig.json");
default_path.exists().then_some(default_path)
});
let config = if let Some(path) = tsconfig_path.as_ref() {
Some(load_tsconfig(path)?)
} else {
None
};
let mut resolved = resolve_compiler_options(
config
.as_ref()
.and_then(|cfg| cfg.compiler_options.as_ref()),
)?;
apply_cli_overrides(&mut resolved, args)?;
let mut opts = serde_json::Map::new();
opts.insert(
"target".into(),
serde_json::Value::String(format!("{:?}", resolved.printer.target).to_lowercase()),
);
opts.insert(
"module".into(),
serde_json::Value::String(format!("{:?}", resolved.printer.module).to_lowercase()),
);
if let Some(ref module_resolution) = resolved.module_resolution {
opts.insert(
"moduleResolution".into(),
serde_json::Value::String(format!("{module_resolution:?}").to_lowercase()),
);
}
if let Some(ref out_dir) = resolved.out_dir {
opts.insert(
"outDir".into(),
serde_json::Value::String(out_dir.display().to_string()),
);
}
if let Some(ref root_dir) = resolved.root_dir {
opts.insert(
"rootDir".into(),
serde_json::Value::String(root_dir.display().to_string()),
);
}
if let Some(ref out_file) = resolved.out_file {
opts.insert(
"outFile".into(),
serde_json::Value::String(out_file.display().to_string()),
);
}
if let Some(ref base_url) = resolved.base_url {
opts.insert(
"baseUrl".into(),
serde_json::Value::String(base_url.display().to_string()),
);
}
if let Some(ref declaration_dir) = resolved.declaration_dir {
opts.insert(
"declarationDir".into(),
serde_json::Value::String(declaration_dir.display().to_string()),
);
}
opts.insert("strict".into(), resolved.checker.strict.into());
opts.insert(
"noImplicitAny".into(),
resolved.checker.no_implicit_any.into(),
);
opts.insert(
"strictNullChecks".into(),
resolved.checker.strict_null_checks.into(),
);
opts.insert(
"strictFunctionTypes".into(),
resolved.checker.strict_function_types.into(),
);
opts.insert(
"strictPropertyInitialization".into(),
resolved.checker.strict_property_initialization.into(),
);
opts.insert(
"strictBindCallApply".into(),
resolved.checker.strict_bind_call_apply.into(),
);
opts.insert(
"noImplicitThis".into(),
resolved.checker.no_implicit_this.into(),
);
opts.insert(
"noImplicitReturns".into(),
resolved.checker.no_implicit_returns.into(),
);
opts.insert(
"useUnknownInCatchVariables".into(),
resolved.checker.use_unknown_in_catch_variables.into(),
);
opts.insert(
"noUncheckedIndexedAccess".into(),
resolved.checker.no_unchecked_indexed_access.into(),
);
opts.insert(
"exactOptionalPropertyTypes".into(),
resolved.checker.exact_optional_property_types.into(),
);
opts.insert(
"isolatedModules".into(),
resolved.checker.isolated_modules.into(),
);
opts.insert(
"esModuleInterop".into(),
resolved.checker.es_module_interop.into(),
);
opts.insert(
"allowSyntheticDefaultImports".into(),
resolved.checker.allow_synthetic_default_imports.into(),
);
opts.insert("declaration".into(), resolved.emit_declarations.into());
opts.insert("declarationMap".into(), resolved.declaration_map.into());
opts.insert("sourceMap".into(), resolved.source_map.into());
opts.insert("noEmit".into(), resolved.no_emit.into());
opts.insert("noEmitOnError".into(), resolved.no_emit_on_error.into());
opts.insert(
"removeComments".into(),
resolved.printer.remove_comments.into(),
);
opts.insert(
"noEmitHelpers".into(),
resolved.printer.no_emit_helpers.into(),
);
opts.insert("incremental".into(), resolved.incremental.into());
opts.insert("noCheck".into(), resolved.no_check.into());
let mut top = serde_json::Map::new();
top.insert("compilerOptions".into(), serde_json::Value::Object(opts));
if let Some(ref cfg) = config {
if let Some(ref files) = cfg.files {
top.insert(
"files".into(),
serde_json::Value::Array(
files
.iter()
.map(|f| serde_json::Value::String(f.clone()))
.collect(),
),
);
}
if let Some(ref include) = cfg.include {
top.insert(
"include".into(),
serde_json::Value::Array(
include
.iter()
.map(|f| serde_json::Value::String(f.clone()))
.collect(),
),
);
}
if let Some(ref exclude) = cfg.exclude {
top.insert(
"exclude".into(),
serde_json::Value::Array(
exclude
.iter()
.map(|f| serde_json::Value::String(f.clone()))
.collect(),
),
);
}
}
let json = serde_json::Value::Object(top);
println!("{}", serde_json::to_string_pretty(&json).unwrap());
Ok(())
}
fn handle_list_files_only(args: &CliArgs, cwd: &std::path::Path) -> Result<()> {
use tsz_cli::config::{load_tsconfig, resolve_compiler_options};
use tsz_cli::driver::apply_cli_overrides;
use tsz_cli::fs::{FileDiscoveryOptions, discover_ts_files};
let tsconfig_path = args
.project
.as_ref()
.map(|p| {
if p.is_dir() {
p.join("tsconfig.json")
} else {
p.clone()
}
})
.or_else(|| {
let default_path = cwd.join("tsconfig.json");
default_path.exists().then_some(default_path)
});
let config = if let Some(path) = tsconfig_path.as_ref() {
Some(load_tsconfig(path)?)
} else {
None
};
let mut resolved = resolve_compiler_options(
config
.as_ref()
.and_then(|cfg| cfg.compiler_options.as_ref()),
)?;
apply_cli_overrides(&mut resolved, args)?;
let base_dir = tsconfig_path
.as_ref()
.and_then(|p| p.parent())
.unwrap_or(cwd);
let files: Vec<std::path::PathBuf> = if !args.files.is_empty() {
args.files.clone()
} else if let Some(ref cfg) = config {
cfg.files
.as_ref()
.map(|f| f.iter().map(std::path::PathBuf::from).collect())
.unwrap_or_default()
} else {
Vec::new()
};
let discovery = FileDiscoveryOptions {
base_dir: base_dir.to_path_buf(),
files,
include: config.as_ref().and_then(|c| c.include.clone()),
exclude: config.as_ref().and_then(|c| c.exclude.clone()),
out_dir: resolved.out_dir.clone(),
follow_links: false,
allow_js: resolved.allow_js,
};
let files = discover_ts_files(&discovery)?;
for file in files {
println!("{}", file.display());
}
Ok(())
}
fn handle_all() -> Result<()> {
use clap::CommandFactory;
println!("tsz: The TypeScript Compiler - Codename Zang\n");
println!("ALL COMPILER OPTIONS\n");
let mut cmd = tsz_cli::args::CliArgs::command();
let help = cmd.render_long_help();
println!("{help}");
println!(
"\nYou can learn about all of the compiler options at https://www.typescriptlang.org/tsconfig"
);
Ok(())
}
fn handle_build(args: &CliArgs, cwd: &std::path::Path) -> Result<()> {
use tsz::checker::diagnostics::DiagnosticCategory;
use tsz_cli::build;
use tsz_cli::project_refs::ProjectReferenceGraph;
let tsconfig_path = args
.project
.as_ref()
.map(|p| {
if p.is_dir() {
p.join("tsconfig.json")
} else {
p.clone()
}
})
.or_else(|| {
let default_path = cwd.join("tsconfig.json");
default_path.exists().then_some(default_path)
});
let Some(ref root_config_path) = tsconfig_path else {
anyhow::bail!("No tsconfig.json found. Use --project to specify one.");
};
let graph = match ProjectReferenceGraph::load(root_config_path) {
Ok(g) => g,
Err(e) => {
println!("Warning: Could not load project references: {e}");
return handle_build_single_project(args, cwd, root_config_path);
}
};
if args.clean {
return handle_build_clean(&graph, args.build_verbose);
}
let build_order: Vec<tsz_cli::project_refs::ProjectId> = match graph.build_order() {
Ok(order) => order,
Err(e) => {
println!("Error: {e}");
std::process::exit(EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED);
}
};
if args.dry {
println!(
"Dry run - would build {} project(s) in order:",
build_order.len()
);
for (i, project_id) in build_order.iter().enumerate() {
if let Some(project) = graph.get_project(*project_id) {
println!(" {}. {}", i + 1, project.config_path.display());
}
}
return Ok(());
}
let mut total_errors = 0;
let mut built_count = 0;
let mut skipped_count = 0;
let pretty = args
.pretty
.unwrap_or_else(|| std::io::stderr().is_terminal());
let mut reporter = Reporter::new(pretty);
if args.build_verbose {
println!("Checking {} project(s)...", build_order.len());
}
for project_id in &build_order {
let Some(project) = graph.get_project(*project_id) else {
continue;
};
if !args.force && build::is_project_up_to_date(project, args) {
if args.build_verbose {
println!("✓ Up to date: {}", project.config_path.display());
}
skipped_count += 1;
continue;
}
if args.build_verbose {
println!("\nBuilding: {}", project.config_path.display());
}
let project_cwd = project.root_dir.clone();
let result = driver::compile_project(args, &project_cwd, &project.config_path)?;
let error_count = result
.diagnostics
.iter()
.filter(|d| d.category == DiagnosticCategory::Error)
.count();
if error_count > 0 {
total_errors += error_count;
if !result.diagnostics.is_empty() {
let output = reporter.render(&result.diagnostics);
if !output.is_empty() {
print!("{output}");
}
}
if args.stop_build_on_errors {
println!(
"\nBuild stopped due to errors in {}",
project.config_path.display()
);
std::process::exit(EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED);
}
}
built_count += 1;
}
if args.build_verbose {
println!(
"\nBuilt {built_count} project(s), skipped {skipped_count} up-to-date project(s), {total_errors} error(s)"
);
}
if total_errors > 0 {
std::process::exit(if built_count > 0 {
EXIT_DIAGNOSTICS_OUTPUTS_GENERATED
} else {
EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED
});
}
Ok(())
}
fn handle_build_clean(
graph: &tsz_cli::project_refs::ProjectReferenceGraph,
verbose: bool,
) -> Result<()> {
use std::fs;
use tsz_cli::config::resolve_compiler_options;
let mut deleted_count = 0;
for project in graph.projects() {
let base_dir = &project.root_dir;
let buildinfo_path = project.config_path.with_extension("tsbuildinfo");
if buildinfo_path.exists() {
fs::remove_file(&buildinfo_path)?;
if verbose {
println!("Deleted: {}", buildinfo_path.display());
}
deleted_count += 1;
}
let resolved = resolve_compiler_options(project.config.base.compiler_options.as_ref())?;
if let Some(ref out_dir) = resolved.out_dir {
let full_out_dir = base_dir.join(out_dir);
if full_out_dir.exists() {
fs::remove_dir_all(&full_out_dir)?;
if verbose {
println!("Deleted: {}", full_out_dir.display());
}
deleted_count += 1;
}
}
if let Some(ref declaration_dir) = resolved.declaration_dir {
let full_decl_dir = base_dir.join(declaration_dir);
if full_decl_dir.exists() {
fs::remove_dir_all(&full_decl_dir)?;
if verbose {
println!("Deleted: {}", full_decl_dir.display());
}
deleted_count += 1;
}
}
}
println!(
"Build cleaned successfully ({} project(s), {} item(s) deleted).",
graph.project_count(),
deleted_count
);
Ok(())
}
fn handle_build_single_project(
args: &CliArgs,
cwd: &std::path::Path,
config_path: &std::path::Path,
) -> Result<()> {
use tsz::checker::diagnostics::DiagnosticCategory;
let result = driver::compile(args, cwd)?;
if args.build_verbose {
println!("Projects in this build: ");
println!(" * {}", config_path.display());
}
if !result.diagnostics.is_empty() {
let pretty = args
.pretty
.unwrap_or_else(|| std::io::stderr().is_terminal());
let mut reporter = Reporter::new(pretty);
let output = reporter.render(&result.diagnostics);
if !output.is_empty() {
print!("{output}");
}
}
let has_errors = result
.diagnostics
.iter()
.any(|d| d.category == DiagnosticCategory::Error);
if has_errors {
std::process::exit(if result.emitted_files.is_empty() {
EXIT_DIAGNOSTICS_OUTPUTS_SKIPPED
} else {
EXIT_DIAGNOSTICS_OUTPUTS_GENERATED
});
}
Ok(())
}
#[cfg(test)]
#[path = "tsz/tests.rs"]
mod tests;