compiler/crates/relay-bin/src/main.rs (235 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ use clap::{ArgEnum, Parser}; use common::ConsoleLogger; use log::{error, info}; use relay_compiler::{ build_project::artifact_writer::ArtifactValidationWriter, compiler::Compiler, config::Config, FileSourceKind, LocalPersister, OperationPersister, PersistConfig, RemotePersister, }; use relay_lsp::{start_language_server, DummyExtraDataProvider}; use schema::SDLSchema; use schema_documentation::SchemaDocumentationLoader; use simplelog::{ ColorChoice, ConfigBuilder as SimpleLogConfigBuilder, LevelFilter, TermLogger, TerminalMode, }; use std::{ env::{self, current_dir}, path::PathBuf, process::Command, sync::Arc, }; mod errors; use errors::Error; #[derive(Parser)] #[clap( name = "Relay Compiler", version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"), about = "Compiles Relay files and writes generated files.", rename_all = "camel_case", args_conflicts_with_subcommands = true )] struct Opt { #[clap(subcommand)] command: Option<Commands>, #[clap(flatten)] compile: CompileCommand, } #[derive(Parser)] #[clap( rename_all = "camel_case", about = "Compiles Relay files and writes generated files." )] struct CompileCommand { /// Compile and watch for changes #[clap(long, short)] watch: bool, /// Compile using this config file. If not provided, searches for a config in /// package.json under the `relay` key or `relay.config.json` files among other up /// from the current working directory. config: Option<PathBuf>, #[clap(flatten)] cli_config: CliConfig, /// Run the persister even if the query has not changed. #[clap(long)] repersist: bool, /// Verbosity level #[clap(long, arg_enum, default_value = "verbose")] output: OutputKind, /// Looks for pending changes and exits with non-zero code instead of /// writing to disk #[clap(long)] validate: bool, } #[derive(Parser)] #[clap( about = "Run the language server. Used by IDEs.", rename_all = "camel_case" )] struct LspCommand { /// Run the LSP using this config file. If not provided, searches for a config in /// package.json under the `relay` key or `relay.config.json` files among other up /// from the current working directory. config: Option<PathBuf>, /// Verbosity level #[clap(long, arg_enum, default_value = "quiet-with-errors")] output: OutputKind, } #[derive(clap::Subcommand)] enum Commands { Compiler(CompileCommand), Lsp(LspCommand), } #[derive(ArgEnum, Clone, Copy)] enum OutputKind { Debug, Quiet, QuietWithErrors, Verbose, } #[derive(Parser)] #[clap(rename_all = "camel_case")] pub struct CliConfig { /// Path for the directory where to search for source code #[clap(long)] pub src: Option<String>, /// Path to schema file #[clap(long)] pub schema: Option<String>, /// Path to a directory, where the compiler should write artifacts #[clap(long)] pub artifact_directory: Option<String>, } impl CliConfig { fn is_defined(&self) -> bool { self.src.is_some() || self.schema.is_some() || self.artifact_directory.is_some() } fn get_config_string(self) -> String { let src = self.src.unwrap_or_else(|| "./src".into()); let schema = self.schema.unwrap_or_else(|| "./path-to-schema".into()); let artifact_directory = self.artifact_directory.map_or("".to_string(), |a| { format!("\n \"artifactDirectory\": \"{}\",", a) }); format!( r#" {{ "src": "{}", "schema": "{}",{} "language": "javascript" }}"#, src, schema, artifact_directory ) } } #[tokio::main] async fn main() { let opt = Opt::parse(); let command = opt.command.unwrap_or(Commands::Compiler(opt.compile)); let result = match command { Commands::Compiler(command) => handle_compiler_command(command).await, Commands::Lsp(command) => handle_lsp_command(command).await, }; match result { Ok(_) => info!("Done."), Err(err) => { error!("{:?}", err); std::process::exit(1); } } } fn get_config(config_path: Option<PathBuf>) -> Result<Config, Error> { match config_path { Some(config_path) => Config::load(config_path).map_err(|err| Error::ConfigError { details: format!("{:?}", err), }), None => Config::search(&current_dir().expect("Unable to get current working directory.")) .map_err(|err| Error::ConfigError { details: format!("{:?}", err), }), } } fn configure_logger(output: OutputKind, terminal_mode: TerminalMode) { let log_level = match output { OutputKind::Debug => LevelFilter::Debug, OutputKind::Quiet => LevelFilter::Off, OutputKind::QuietWithErrors => LevelFilter::Error, OutputKind::Verbose => LevelFilter::Info, }; let log_config = SimpleLogConfigBuilder::new() .set_time_level(LevelFilter::Off) .set_target_level(LevelFilter::Off) .set_location_level(LevelFilter::Off) .set_thread_level(LevelFilter::Off) .build(); TermLogger::init(log_level, log_config, terminal_mode, ColorChoice::Auto).unwrap(); } async fn handle_compiler_command(command: CompileCommand) -> Result<(), Error> { configure_logger(command.output, TerminalMode::Mixed); if command.cli_config.is_defined() { return Err(Error::ConfigError { details: format!( "\nPassing Relay compiler configuration is not supported. Please add `relay.config.json` file,\nor \"relay\" section to your `package.json` file.\n\nCompiler configuration JSON:{}", command.cli_config.get_config_string(), ), }); } let mut config = get_config(command.config)?; if command.validate { config.artifact_writer = Box::new(ArtifactValidationWriter::default()); } config.create_operation_persister = Some(Box::new(|project_config| { project_config.persist.as_ref().map( |persist_config| -> Box<dyn OperationPersister + Send + Sync> { match persist_config { PersistConfig::Remote(remote_config) => { Box::new(RemotePersister::new(remote_config.clone())) } PersistConfig::Local(local_config) => { Box::new(LocalPersister::new(local_config.clone())) } } }, ) })); config.file_source_config = if should_use_watchman() { FileSourceKind::Watchman } else { FileSourceKind::WalkDir }; config.repersist_operations = command.repersist; if command.watch && !matches!(&config.file_source_config, FileSourceKind::Watchman) { panic!( "Cannot run relay in watch mode if `watchman` is not available (or explicitly disabled)." ); } let compiler = Compiler::new(Arc::new(config), Arc::new(ConsoleLogger)); if command.watch { compiler.watch().await.map_err(|err| Error::CompilerError { details: format!("{:?}", err), })?; } else { compiler .compile() .await .map_err(|err| Error::CompilerError { details: format!("{:?}", err), })?; } Ok(()) } async fn handle_lsp_command(command: LspCommand) -> Result<(), Error> { configure_logger(command.output, TerminalMode::Stderr); let config = get_config(command.config)?; let perf_logger = Arc::new(ConsoleLogger); let extra_data_provider = Box::new(DummyExtraDataProvider::new()); let schema_documentation_loader: Option<Box<dyn SchemaDocumentationLoader<SDLSchema>>> = None; let js_language_server = None; start_language_server( config, perf_logger, extra_data_provider, schema_documentation_loader, js_language_server, ) .await .map_err(|err| Error::LSPError { details: format!("Relay LSP unexpectedly terminated: {:?}", err), })?; info!("Relay LSP exited successfully."); Ok(()) } /// Check if `watchman` is available. /// Additionally, this method is checking for an existence of `FORCE_NO_WATCHMAN` /// environment variable. If this `FORCE_NO_WATCHMAN` is set, this method will return `false` /// and compiler will use non-watchman file finder. fn should_use_watchman() -> bool { let check_watchman = Command::new("watchman") .args(["list-capabilities"]) .output(); check_watchman.is_ok() && env::var("FORCE_NO_WATCHMAN").is_err() }