diff --git a/Cargo.lock b/Cargo.lock index 5f72d4630cb..8d670275b3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,6 +1510,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "smallvec", "string_wizard", "sugar_path", "testing_macros", @@ -1776,9 +1777,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smawk" diff --git a/Cargo.toml b/Cargo.toml index 9ba8c8f5c50..902966d23d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ resolver = "2" [workspace.package] -edition = "2021" -homepage = "https://github.com/rolldown-rs/rolldown" -license = "MIT" -repository = "https://github.com/rolldown-rs/rolldown" +edition = "2021" +homepage = "https://github.com/rolldown-rs/rolldown" +license = "MIT" +repository = "https://github.com/rolldown-rs/rolldown" [workspace.dependencies] oxc = { version = "0.1.3", features = ["semantic", "formatter"] } @@ -23,9 +23,10 @@ serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.87" insta = "1.21.0" testing_macros = "0.2.7" -scoped-tls = "1.0" +scoped-tls = "1.0.1" string_wizard = { version = "0.0.9" } async-trait = "0.1.62" futures = "0.3.25" itertools = "0.10.5" thiserror = "1.0.50" +smallvec = "1.11.1" diff --git a/crates/rolldown/Cargo.toml b/crates/rolldown/Cargo.toml index ca8c4ec89fe..3fb4c223f33 100644 --- a/crates/rolldown/Cargo.toml +++ b/crates/rolldown/Cargo.toml @@ -31,6 +31,7 @@ futures = "0.3.25" rayon = "1.6.0" string_wizard = { workspace = true } async-trait = { workspace = true } +smallvec = { workspace = true } [dev_dependencies] insta = { workspace = true } diff --git a/crates/rolldown/src/bundler/bundle/bundle.rs b/crates/rolldown/src/bundler/bundle/bundle.rs index 808d5b8575d..4dd0ee71250 100644 --- a/crates/rolldown/src/bundler/bundle/bundle.rs +++ b/crates/rolldown/src/bundler/bundle/bundle.rs @@ -15,7 +15,6 @@ use crate::bundler::{ }, utils::bitset::BitSet, }; -use anyhow::Ok; use index_vec::{index_vec, IndexVec}; use rolldown_common::{ImportKind, ModuleId, SymbolRef}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -254,10 +253,7 @@ impl<'a> Bundle<'a> { ChunkGraph { chunks, module_to_chunk } } - pub fn generate( - &mut self, - _input_options: &'a NormalizedInputOptions, - ) -> anyhow::Result> { + pub fn generate(&mut self, _input_options: &'a NormalizedInputOptions) -> Vec { use rayon::prelude::*; let mut chunk_graph = self.generate_chunks(); @@ -284,6 +280,6 @@ impl<'a> Bundle<'a> { }) .collect::>(); - Ok(assets) + assets } } diff --git a/crates/rolldown/src/bundler/bundler.rs b/crates/rolldown/src/bundler/bundler.rs index 5b02316817d..2ad3c6bcce9 100644 --- a/crates/rolldown/src/bundler/bundler.rs +++ b/crates/rolldown/src/bundler/bundler.rs @@ -1,3 +1,4 @@ +use rolldown_error::BuildError; use sugar_path::AsPath; use super::{ @@ -10,6 +11,8 @@ use super::{ }; use crate::{bundler::bundle::bundle::Bundle, plugin::plugin::BoxPlugin, InputOptions}; +type BuildResult = Result>; + pub struct Bundler { input_options: NormalizedInputOptions, _plugins: Vec, @@ -28,10 +31,7 @@ impl Bundler { Self { input_options: normalized, _plugins: plugins } } - pub async fn write( - &mut self, - output_options: crate::OutputOptions, - ) -> anyhow::Result> { + pub async fn write(&mut self, output_options: crate::OutputOptions) -> BuildResult> { let dir = output_options.dir.clone().unwrap_or_else(|| { self.input_options.cwd.as_path().join("dist").to_string_lossy().to_string() }); @@ -50,7 +50,7 @@ impl Bundler { let dest = dir.as_path().join(&chunk.file_name); if let Some(p) = dest.parent() { if !p.exists() { - std::fs::create_dir_all(p)?; + std::fs::create_dir_all(p).unwrap(); } }; std::fs::write(dest, &chunk.content).unwrap_or_else(|_| { @@ -64,12 +64,12 @@ impl Bundler { pub async fn generate( &mut self, output_options: crate::OutputOptions, - ) -> anyhow::Result> { + ) -> BuildResult> { let normalized = NormalizedOutputOptions::from_output_options(output_options); self.build(normalized).await } - async fn build(&mut self, output_options: NormalizedOutputOptions) -> anyhow::Result> { + async fn build(&mut self, output_options: NormalizedOutputOptions) -> BuildResult> { tracing::trace!("NormalizedInputOptions {:#?}", self.input_options); tracing::trace!("NormalizedOutputOptions: {output_options:#?}",); @@ -77,7 +77,7 @@ impl Bundler { graph.generate_module_graph(&self.input_options).await?; let mut bundle = Bundle::new(&mut graph, &output_options); - let assets = bundle.generate(&self.input_options)?; + let assets = bundle.generate(&self.input_options); Ok(assets) } diff --git a/crates/rolldown/src/bundler/chunk/chunk.rs b/crates/rolldown/src/bundler/chunk/chunk.rs index 5ff1413b0ba..c5e8959cece 100644 --- a/crates/rolldown/src/bundler/chunk/chunk.rs +++ b/crates/rolldown/src/bundler/chunk/chunk.rs @@ -3,14 +3,17 @@ use rolldown_common::{ModuleId, SymbolRef}; use rustc_hash::FxHashMap; use string_wizard::{Joiner, JoinerOptions}; -use crate::bundler::{ - chunk_graph::ChunkGraph, - graph::graph::Graph, - module::ModuleRenderContext, - options::{ - file_name_template::FileNameRenderOptions, normalized_output_options::NormalizedOutputOptions, +use crate::{ + bundler::{ + chunk_graph::ChunkGraph, + graph::graph::Graph, + module::ModuleRenderContext, + options::{ + file_name_template::FileNameRenderOptions, normalized_output_options::NormalizedOutputOptions, + }, + utils::bitset::BitSet, }, - utils::bitset::BitSet, + error::BatchedResult, }; use super::ChunkId; @@ -54,7 +57,7 @@ impl Chunk { } #[allow(clippy::unnecessary_wraps)] - pub fn render(&self, graph: &Graph, chunk_graph: &ChunkGraph) -> anyhow::Result { + pub fn render(&self, graph: &Graph, chunk_graph: &ChunkGraph) -> BatchedResult { use rayon::prelude::*; let mut joiner = Joiner::with_options(JoinerOptions { separator: Some("\n".to_string()) }); joiner.append(self.render_imports_for_esm(graph, chunk_graph)); diff --git a/crates/rolldown/src/bundler/graph/graph.rs b/crates/rolldown/src/bundler/graph/graph.rs index f7c4bfa89b1..afc4c5a377b 100644 --- a/crates/rolldown/src/bundler/graph/graph.rs +++ b/crates/rolldown/src/bundler/graph/graph.rs @@ -1,7 +1,10 @@ use super::{linker::Linker, linker_info::LinkingInfoVec, symbols::Symbols}; -use crate::bundler::{ - module::ModuleVec, module_loader::ModuleLoader, - options::normalized_input_options::NormalizedInputOptions, runtime::Runtime, +use crate::{ + bundler::{ + module::ModuleVec, module_loader::ModuleLoader, + options::normalized_input_options::NormalizedInputOptions, runtime::Runtime, + }, + error::BatchedResult, }; use rolldown_common::ModuleId; use rustc_hash::FxHashSet; @@ -20,7 +23,7 @@ impl Graph { pub async fn generate_module_graph( &mut self, input_options: &NormalizedInputOptions, - ) -> anyhow::Result<()> { + ) -> BatchedResult<()> { ModuleLoader::new(input_options, self).fetch_all_modules().await?; tracing::trace!("{:#?}", self); diff --git a/crates/rolldown/src/bundler/module_loader/module_loader.rs b/crates/rolldown/src/bundler/module_loader/module_loader.rs index 31bf8ff3a5b..3a45e597aa0 100644 --- a/crates/rolldown/src/bundler/module_loader/module_loader.rs +++ b/crates/rolldown/src/bundler/module_loader/module_loader.rs @@ -18,6 +18,7 @@ use crate::bundler::module::Module; use crate::bundler::options::normalized_input_options::NormalizedInputOptions; use crate::bundler::runtime::RUNTIME_PATH; use crate::bundler::utils::resolve_id::{resolve_id, ResolvedRequestInfo}; +use crate::error::{BatchedErrors, BatchedResult}; use crate::SharedResolver; pub struct ModuleLoader<'a> { @@ -44,12 +45,10 @@ impl<'a> ModuleLoader<'a> { } } - pub async fn fetch_all_modules(&mut self) -> anyhow::Result<()> { - if self.input_options.input.is_empty() { - return Err(anyhow::format_err!("You must supply options.input to rolldown")); - } + pub async fn fetch_all_modules(&mut self) -> BatchedResult<()> { + assert!(!self.input_options.input.is_empty(), "You must supply options.input to rolldown"); - let resolved_entries = self.resolve_entries(); + let resolved_entries = self.resolve_entries()?; let mut intermediate_modules: IndexVec> = IndexVec::with_capacity(resolved_entries.len() + 1 /* runtime */); @@ -126,13 +125,13 @@ impl<'a> ModuleLoader<'a> { } #[allow(clippy::collection_is_never_read)] - fn resolve_entries(&mut self) -> Vec<(Option, ResolvedRequestInfo)> { + fn resolve_entries(&mut self) -> BatchedResult, ResolvedRequestInfo)>> { let resolver = &self.resolver; let resolved_ids = block_on_spawn_all(self.input_options.input.iter().map(|input_item| async move { let specifier = &input_item.import; - let resolve_id = resolve_id(resolver, specifier, None, false).await.unwrap(); + let resolve_id = resolve_id(resolver, specifier, None, false).await?; let Some(info) = resolve_id else { return Err(BuildError::unresolved_entry(specifier)); @@ -145,18 +144,16 @@ impl<'a> ModuleLoader<'a> { Ok((input_item.name.clone(), info)) })); - let mut errors = vec![]; + let mut errors = BatchedErrors::default(); - resolved_ids - .into_iter() - .filter_map(|handle| match handle { - Ok(id) => Some(id), - Err(e) => { - errors.push(e); - None - } - }) - .collect() + let collected = + resolved_ids.into_iter().filter_map(|item| errors.take_err_from(item)).collect(); + + if errors.is_empty() { + Ok(collected) + } else { + Err(errors) + } } fn try_spawn_new_task( diff --git a/crates/rolldown/src/error.rs b/crates/rolldown/src/error.rs new file mode 100644 index 00000000000..b7320948e73 --- /dev/null +++ b/crates/rolldown/src/error.rs @@ -0,0 +1,44 @@ +use rolldown_error::BuildError; +use smallvec::SmallVec; + +#[derive(Debug, Default)] +pub struct BatchedErrors(SmallVec<[BuildError; 1]>); + +impl BatchedErrors { + pub fn with_error(err: BuildError) -> Self { + Self(smallvec::smallvec![err]) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&mut self, err: BuildError) { + self.0.push(err); + } + + /// Try to take the Err() of the given result and return Some(T) if it's Ok(T). + pub fn take_err_from(&mut self, res: Result) -> Option { + match res { + Ok(t) => Some(t), + Err(err) => { + self.push(err); + None + } + } + } +} + +pub type BatchedResult = Result; + +impl From for BatchedErrors { + fn from(err: BuildError) -> Self { + Self::with_error(err) + } +} + +impl From for Vec { + fn from(errs: BatchedErrors) -> Self { + errs.0.into_vec() + } +} diff --git a/crates/rolldown/src/lib.rs b/crates/rolldown/src/lib.rs index 3053e7729d8..2d39e018ee9 100644 --- a/crates/rolldown/src/lib.rs +++ b/crates/rolldown/src/lib.rs @@ -1,4 +1,5 @@ mod bundler; +mod error; mod plugin; use std::sync::Arc; diff --git a/crates/rolldown/src/plugin/plugin.rs b/crates/rolldown/src/plugin/plugin.rs index 898945e17ee..dcb993af8c8 100644 --- a/crates/rolldown/src/plugin/plugin.rs +++ b/crates/rolldown/src/plugin/plugin.rs @@ -1,14 +1,16 @@ use std::{borrow::Cow, fmt::Debug}; +use crate::error::BatchedResult; + use super::{ args::{HookLoadArgs, HookResolveIdArgs, HookTransformArgs}, context::PluginContext, output::{HookLoadOutput, HookResolveIdOutput}, }; -pub type HookResolveIdReturn = rolldown_error::BuildResult>; -pub type HookTransformReturn = rolldown_error::BuildResult>; -pub type HookLoadReturn = rolldown_error::BuildResult>; +pub type HookResolveIdReturn = BatchedResult>; +pub type HookTransformReturn = BatchedResult>; +pub type HookLoadReturn = BatchedResult>; #[async_trait::async_trait] pub trait Plugin: Debug + Send + Sync { diff --git a/crates/rolldown/tests/common/case.rs b/crates/rolldown/tests/common/case.rs index a474444b208..1e2d4e5aa69 100644 --- a/crates/rolldown/tests/common/case.rs +++ b/crates/rolldown/tests/common/case.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, path::Path}; use rolldown::Asset; +use rolldown_error::BuildError; use string_wizard::MagicString; use super::fixture::Fixture; @@ -21,13 +22,26 @@ impl Case { } pub async fn run_inner(mut self) { - let assets = self.fixture.compile().await; - let snapshot = self.convert_assets_to_snapshot(assets); + let build_output = self.fixture.compile().await; + match build_output { + Ok(assets) => { + if self.fixture.test_config().expect_error { + panic!("expected error, but got success") + } + self.render_assets_to_snapshot(assets); + } + Err(errs) => { + if !self.fixture.test_config().expect_error { + panic!("expected success, but got errors: {:?}", errs) + } + self.render_errors_to_snapshot(errs); + } + } self.make_snapshot(); self.fixture.exec(); } - fn convert_assets_to_snapshot(&mut self, mut assets: Vec) { + fn render_assets_to_snapshot(&mut self, mut assets: Vec) { self.snapshot.append("# Assets\n\n"); assets.sort_by_key(|c| c.file_name.clone()); let artifacts = assets @@ -47,6 +61,24 @@ impl Case { self.snapshot.append(artifacts); } + fn render_errors_to_snapshot(&mut self, mut errors: Vec) { + self.snapshot.append("# Errors\n\n"); + errors.sort_by_key(|e| e.code()); + let rendered = errors + .iter() + .flat_map(|error| { + [ + Cow::Owned(format!("## {}\n", error.code())), + "```text".into(), + Cow::Owned(error.to_string()), + "```".into(), + ] + }) + .collect::>() + .join("\n"); + self.snapshot.append(rendered); + } + fn make_snapshot(&self) { // Configure insta to use the fixture path as the snapshot path let fixture_folder = self.fixture.dir_path(); diff --git a/crates/rolldown/tests/common/fixture.rs b/crates/rolldown/tests/common/fixture.rs index 1b2e332165f..978e13c36e3 100644 --- a/crates/rolldown/tests/common/fixture.rs +++ b/crates/rolldown/tests/common/fixture.rs @@ -5,6 +5,7 @@ use std::{ }; use rolldown::{Asset, Bundler, InputOptions, OutputOptions}; +use rolldown_error::BuildError; use rolldown_testing::TestConfig; fn default_test_input_item() -> rolldown_testing::InputItem { @@ -34,14 +35,14 @@ impl Fixture { &self.fixture_path } - fn test_config(&self) -> TestConfig { + pub fn test_config(&self) -> TestConfig { TestConfig::from_config_path(&self.config_path()) } pub fn exec(&self) { let test_config = self.test_config(); - if !test_config.expect_executed { + if !test_config.expect_executed || test_config.expect_error { return; } @@ -79,7 +80,7 @@ impl Fixture { } } - pub async fn compile(&mut self) -> Vec { + pub async fn compile(&mut self) -> Result, Vec> { let fixture_path = self.dir_path(); let mut test_config = self.test_config(); @@ -109,6 +110,5 @@ impl Fixture { ..Default::default() }) .await - .unwrap() } } diff --git a/crates/rolldown/tests/fixtures/errors/unresolved_entry/artifacts.snap b/crates/rolldown/tests/fixtures/errors/unresolved_entry/artifacts.snap new file mode 100644 index 00000000000..3e7083f839b --- /dev/null +++ b/crates/rolldown/tests/fixtures/errors/unresolved_entry/artifacts.snap @@ -0,0 +1,12 @@ +--- +source: crates/rolldown/tests/common/case.rs +expression: content +input_file: crates/rolldown/tests/fixtures/errors/unresolved_entry +--- +# Errors + +## UNRESOLVED_ENTRY + +```text +Could not resolve entry module "tests/fixtures/errors/unresolved_entry/main.js" +``` diff --git a/crates/rolldown/tests/fixtures/errors/unresolved_entry/test.config.json b/crates/rolldown/tests/fixtures/errors/unresolved_entry/test.config.json new file mode 100644 index 00000000000..a3af7311982 --- /dev/null +++ b/crates/rolldown/tests/fixtures/errors/unresolved_entry/test.config.json @@ -0,0 +1,3 @@ +{ + "expectError": true +} \ No newline at end of file diff --git a/crates/rolldown_error/src/error/mod.rs b/crates/rolldown_error/src/error/mod.rs index 1dc28da4bb9..abbbcfbbd9b 100644 --- a/crates/rolldown_error/src/error/mod.rs +++ b/crates/rolldown_error/src/error/mod.rs @@ -26,7 +26,6 @@ pub enum BuildError { ExternalEntry(Box), #[error(transparent)] UnresolvedImport(Box), - // TODO: probably should remove this error #[error("Napi error: {status}: {reason}")] Napi { status: String, reason: String }, diff --git a/crates/rolldown_error/src/error/unresolved_entry.rs b/crates/rolldown_error/src/error/unresolved_entry.rs index 7ac2775d57d..cc6a4e6b83c 100644 --- a/crates/rolldown_error/src/error/unresolved_entry.rs +++ b/crates/rolldown_error/src/error/unresolved_entry.rs @@ -1,8 +1,8 @@ +use crate::PathExt; use std::path::PathBuf; use thiserror::Error; - #[derive(Error, Debug)] -#[error("Could not resolve entry module {:?}", unresolved_id)] +#[error("Could not resolve entry module {:?}", unresolved_id.relative_display())] pub struct UnresolvedEntry { pub(crate) unresolved_id: PathBuf, } diff --git a/crates/rolldown_error/src/lib.rs b/crates/rolldown_error/src/lib.rs index 9ae6599e6ce..9a031950660 100644 --- a/crates/rolldown_error/src/lib.rs +++ b/crates/rolldown_error/src/lib.rs @@ -1,6 +1,24 @@ mod error; mod error_code; mod utils; +use std::path::Path; + +use sugar_path::SugarPath; + pub use crate::error::BuildError; -pub type BuildResult = Result; +trait PathExt { + fn relative_display(&self) -> String; +} + +impl PathExt for Path { + fn relative_display(&self) -> String { + // FIXME: we should use the same cwd as the user passed to rolldown + let cwd = std::env::current_dir().unwrap(); + if self.is_absolute() { + self.relative(cwd).display().to_string() + } else { + self.display().to_string() + } + } +} diff --git a/crates/rolldown_node_binding/src/bundler.rs b/crates/rolldown_node_binding/src/bundler.rs index 6a0beda788c..73df40d419b 100644 --- a/crates/rolldown_node_binding/src/bundler.rs +++ b/crates/rolldown_node_binding/src/bundler.rs @@ -52,7 +52,18 @@ impl Bundler { let binding_opts = resolve_output_options(opts)?; - let outputs = bundler_core.write(binding_opts).await.map_err(|err| self.handle_errors(err))?; + let maybe_outputs = bundler_core.write(binding_opts).await; + + let outputs = match maybe_outputs { + Ok(outputs) => outputs, + Err(err) => { + // TODO: better handing errors + for err in err { + eprintln!("{err:?}"); + } + return Err(napi::Error::from_reason("Build failed")); + } + }; let output_chunks = outputs .into_iter() @@ -70,8 +81,18 @@ impl Bundler { let binding_opts = resolve_output_options(opts)?; - let outputs = - bundler_core.generate(binding_opts).await.map_err(|err| self.handle_errors(err))?; + let maybe_outputs = bundler_core.generate(binding_opts).await; + + let outputs = match maybe_outputs { + Ok(outputs) => outputs, + Err(err) => { + // TODO: better handing errors + for err in err { + eprintln!("{err:?}"); + } + return Err(napi::Error::from_reason("Build failed")); + } + }; let output_chunks = outputs .into_iter() @@ -79,10 +100,4 @@ impl Bundler { .collect::>(); Ok(output_chunks) } - - #[allow(clippy::needless_pass_by_value, clippy::unused_self)] - fn handle_errors(&self, error: anyhow::Error) -> napi::Error { - eprintln!("{error}"); - napi::Error::from_reason("Build failed") - } } diff --git a/crates/rolldown_resolver/src/lib.rs b/crates/rolldown_resolver/src/lib.rs index 9829268113a..296ae883a67 100644 --- a/crates/rolldown_resolver/src/lib.rs +++ b/crates/rolldown_resolver/src/lib.rs @@ -4,6 +4,7 @@ use std::{ borrow::Cow, path::{Path, PathBuf}, }; +use sugar_path::SugarPathBuf; use oxc_resolver::{Resolution, ResolveOptions, Resolver as OxcResolver}; @@ -59,7 +60,7 @@ impl Resolver { // In this case, we couldn't simply use the CWD as the importer. // Instead, we should concat the CWD with the specifier. This aligns with https://github.com/rollup/rollup/blob/680912e2ceb42c8d5e571e01c6ece0e4889aecbb/src/utils/resolveId.ts#L56. let specifier = if importer.is_none() { - Cow::Owned(self.cwd.join(specifier)) + Cow::Owned(self.cwd.join(specifier).into_normalize()) } else { Cow::Borrowed(Path::new(specifier)) }; @@ -75,16 +76,15 @@ impl Resolver { resolved: info.path().to_string_lossy().to_string().into(), module_type: calc_module_type(&info), }), - Err(_err) => { - if let Some(importer) = importer { + Err(_err) => importer.map_or_else( + || Err(BuildError::unresolved_entry(specifier.to_str().unwrap())), + |importer| { Err(BuildError::unresolved_import( specifier.to_string_lossy().to_string(), importer.prettify(), )) - } else { - Err(BuildError::unresolved_entry(specifier)) - } - } + }, + ), } } } diff --git a/crates/rolldown_testing/src/test_config/mod.rs b/crates/rolldown_testing/src/test_config/mod.rs index 8745e65f7f7..56fe0227698 100644 --- a/crates/rolldown_testing/src/test_config/mod.rs +++ b/crates/rolldown_testing/src/test_config/mod.rs @@ -30,6 +30,9 @@ pub struct TestConfig { #[serde(default = "true_by_default")] /// If `false`, the compiled artifacts won't be executed. pub expect_executed: bool, + #[serde(default)] + /// If `true`, the fixture are expected to fail to compile/build. + pub expect_error: bool, #[serde(default, rename = "_comment")] /// An workaround for writing comments in JSON. pub _comment: String, diff --git a/crates/rolldown_testing/test.config.scheme.json b/crates/rolldown_testing/test.config.scheme.json index 844888d1d67..3fe0dd7a168 100644 --- a/crates/rolldown_testing/test.config.scheme.json +++ b/crates/rolldown_testing/test.config.scheme.json @@ -8,6 +8,11 @@ "default": "", "type": "string" }, + "expectError": { + "description": "If `true`, the fixture are expected to fail to compile/build.", + "default": false, + "type": "boolean" + }, "expectExecuted": { "description": "If `false`, the compiled artifacts won't be executed.", "default": true,