use {
crate::{
cli::Cli,
rcfile::{
error::{NodeJsResult, RcfileError},
Rcfile,
},
},
log::debug,
std::{path::Path, process::Command},
};
pub fn from_javascript_path(file_path: &Path) -> Result<Rcfile, RcfileError> {
let escaped_file_path_for_nodejs = file_path.to_string_lossy().replace('\\', "\\\\");
let nodejs_script = format!(
r#"
import('{escaped_file_path_for_nodejs}')
.then(findConfig)
.then((value) => {{
if (isNonEmptyObject(value)) {{
console.log(JSON.stringify({{
_tag: 'Ok',
value: JSON.stringify(value),
source: 'import',
}}));
}} else {{
tryRequire('Config expected at default export');
}}
}})
.catch((err) => {{
tryRequire(err.stack || err.message || 'Unknown error in import()');
}});
function tryRequire(importError) {{
Promise.resolve(null)
.then(() => require('{escaped_file_path_for_nodejs}'))
.then(findConfig)
.then((value) => {{
if (isNonEmptyObject(value)) {{
console.log(JSON.stringify({{
_tag: 'Ok',
value: JSON.stringify(value),
source: 'require',
}}));
}} else {{
console.log(JSON.stringify({{
_tag: 'Err',
importError,
requireError: 'Config expected at module.exports',
}}));
}}
}})
.catch((err) => {{
console.log(JSON.stringify({{
_tag: 'Err',
importError,
requireError: err.stack || err.message || 'Unknown require error'
}}));
}});
}};
function isNonEmptyObject(value) {{
return value && typeof value === 'object' && value.constructor === Object && Object.keys(value).length > 0;
}}
function findConfig(mod) {{
return mod.default && mod.default.default ? mod.default.default : mod.default;
}}
"#
);
let is_typescript = file_path.to_string_lossy().ends_with("ts");
let mut args = vec![];
if is_typescript {
args.push("--experimental-strip-types");
}
args.push("--eval");
args.push(&nodejs_script);
Command::new("node")
.args(args)
.current_dir(file_path.parent().unwrap_or_else(|| Path::new(".")))
.output()
.map_err(RcfileError::NodeJsExecutionFailed)
.and_then(|output| {
if output.status.success() {
Ok(output.stdout)
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if stderr.contains("experimental-strip-types") {
Err(RcfileError::NodeJsCannotStripTypes { stderr })
} else {
Err(RcfileError::ProcessFailed { stderr })
}
}
})
.and_then(|stdout| String::from_utf8(stdout).map_err(RcfileError::InvalidUtf8))
.inspect(|json_str| {
debug!("Raw output from {:?}: {}", file_path, json_str.trim());
})
.and_then(|json_str| serde_json::from_str::<NodeJsResult>(&json_str).map_err(RcfileError::JsonParseFailed))
.and_then(|response| match response {
NodeJsResult::Success { value } => serde_json::from_str::<Rcfile>(&value).map_err(RcfileError::InvalidConfig),
NodeJsResult::Error {
import_error,
require_error,
} => Err(RcfileError::JavaScriptImportFailed {
import_error,
require_error,
}),
})
}
pub fn try_from_js_candidates(cli: &Cli) -> Option<Result<Rcfile, RcfileError>> {
let candidates = vec![
".synopkgrc.js",
".synopkgrc.ts",
".synopkgrc.mjs",
".synopkgrc.cjs",
"synopkg.config.js",
"synopkg.config.ts",
"synopkg.config.mjs",
"synopkg.config.cjs",
];
for candidate in candidates {
let config_path = cli.cwd.join(candidate);
if config_path.exists() {
debug!("Found JavaScript/TypeScript config file: {config_path:?}");
return Some(from_javascript_path(&config_path));
}
}
None
}