use std::{fs::read_to_string, path::Path};
use anyhow::{Context, Result};
use serde_json::{Value, json, to_string_pretty};
use super::rendered_template::RenderedTemplate;
use crate::constants::TestFramework;
use crate::core::manifest::application::ApplicationManifestData;
pub(crate) fn generate_project_tsconfig(
path_dir: &Path,
extra_types: Option<&[&str]>,
) -> Result<Option<RenderedTemplate>> {
let path = path_dir.join("tsconfig.json");
if path.exists() {
return Ok(None);
}
let mut compiler_options = json!({
"outDir": "dist"
});
if let Some(types) = extra_types {
let mut all_types = vec!["node", "vitest/globals"];
all_types.extend_from_slice(types);
compiler_options["types"] = json!(all_types);
}
Ok(Some(RenderedTemplate {
path,
content: to_string_pretty(&json!({
"extends": "../tsconfig.base.json",
"compilerOptions": compiler_options,
"exclude": [
"node_modules",
"dist",
"eslint.config.mjs"
]
}))?,
context: None,
}))
}
pub(crate) fn generate_root_tsconfig(
path_dir: &Path,
manifest_data: &ApplicationManifestData,
) -> Result<Option<RenderedTemplate>> {
let path = path_dir.join("tsconfig.json");
if path.exists() {
return Ok(None);
}
let references = manifest_data
.projects
.iter()
.map(|project| json!({ "path": project.name }))
.collect::<Vec<Value>>();
Ok(Some(RenderedTemplate {
path,
content: to_string_pretty(&json!({
"files": [],
"references": references,
"compilerOptions": {
"declaration": true,
"declarationMap": true
}
}))?,
context: None,
}))
}
pub(crate) fn generate_modules_tsconfig(
modules_path: &Path,
manifest_data: &ApplicationManifestData,
) -> Result<RenderedTemplate> {
let path = modules_path.join("tsconfig.json");
let references = manifest_data
.projects
.iter()
.filter(|project| project.name != "client-sdk")
.map(|project| json!({ "path": project.name }))
.collect::<Vec<Value>>();
Ok(RenderedTemplate {
path,
content: to_string_pretty(&json!({
"files": [],
"declaration": true,
"declarationMap": true,
"references": references
}))?,
context: None,
})
}
pub(crate) fn add_project_to_modules_tsconfig(
modules_path: &Path,
project_name: &str,
) -> Result<RenderedTemplate> {
let path = modules_path.join("tsconfig.json");
let mut tsconfig: serde_json::Map<String, Value> = if path.exists() {
let content = read_to_string(&path).with_context(|| "Failed to read tsconfig.json")?;
serde_json::from_str(&content).with_context(|| "Failed to parse tsconfig.json")?
} else {
serde_json::Map::from_iter([
("files".to_string(), json!([])),
("declaration".to_string(), json!(true)),
("declarationMap".to_string(), json!(true)),
("references".to_string(), json!([])),
])
};
let references = tsconfig
.entry("references")
.or_insert_with(|| json!([]))
.as_array_mut()
.context("references must be an array")?;
let reference_exists = references
.iter()
.any(|r| r.get("path").and_then(|p| p.as_str()) == Some(project_name));
if !reference_exists {
references.push(json!({ "path": project_name }));
}
Ok(RenderedTemplate {
path,
content: to_string_pretty(&tsconfig)?,
context: None,
})
}
pub(crate) fn remove_project_from_modules_tsconfig(
modules_path: &Path,
project_name: &str,
) -> Result<RenderedTemplate> {
let path = modules_path.join("tsconfig.json");
if !path.exists() {
anyhow::bail!("tsconfig.json does not exist at {:?}", path);
}
let content = read_to_string(&path).with_context(|| "Failed to read tsconfig.json")?;
let mut tsconfig: serde_json::Map<String, Value> =
serde_json::from_str(&content).with_context(|| "Failed to parse tsconfig.json")?;
let references = tsconfig
.get_mut("references")
.and_then(|r| r.as_array_mut())
.context("references must be an array")?;
references.retain(|r| r.get("path").and_then(|p| p.as_str()) != Some(project_name));
Ok(RenderedTemplate {
path,
content: to_string_pretty(&tsconfig)?,
context: None,
})
}
pub(crate) fn update_project_in_modules_tsconfig(
modules_path: &Path,
old_name: &str,
new_name: &str,
) -> Result<RenderedTemplate> {
let path = modules_path.join("tsconfig.json");
if !path.exists() {
anyhow::bail!("tsconfig.json does not exist at {:?}", path);
}
let content = read_to_string(&path).with_context(|| "Failed to read tsconfig.json")?;
let mut tsconfig: serde_json::Map<String, Value> =
serde_json::from_str(&content).with_context(|| "Failed to parse tsconfig.json")?;
let references = tsconfig
.get_mut("references")
.and_then(|r| r.as_array_mut())
.context("references must be an array")?;
for reference in references.iter_mut() {
if let Some(path_value) = reference.get("path") {
if path_value.as_str() == Some(old_name) {
*reference = json!({ "path": new_name });
break;
}
}
}
Ok(RenderedTemplate {
path,
content: to_string_pretty(&tsconfig)?,
context: None,
})
}
fn test_framework_type_entry(test_framework: &TestFramework) -> &'static str {
match test_framework {
TestFramework::Vitest => "vitest/globals",
TestFramework::Jest => "jest",
}
}
const ALL_TEST_FRAMEWORK_TYPE_ENTRIES: &[&str] = &["vitest/globals", "jest"];
fn swap_test_type_in_tsconfig(
tsconfig: &mut serde_json::Map<String, Value>,
new_type: Option<&str>,
) {
if let Some(compiler_options) = tsconfig
.get_mut("compilerOptions")
.and_then(|co| co.as_object_mut())
{
if let Some(types) = compiler_options
.get_mut("types")
.and_then(|t| t.as_array_mut())
{
types.retain(|t| {
t.as_str()
.map(|s| !ALL_TEST_FRAMEWORK_TYPE_ENTRIES.contains(&s))
.unwrap_or(true)
});
if let Some(new_type) = new_type {
if !types.iter().any(|t| t.as_str() == Some(new_type)) {
types.push(json!(new_type));
}
}
}
}
}
pub(crate) fn update_tsconfig_test_framework_types(
base_path: &Path,
new_test_framework: Option<&TestFramework>,
project_names: &[&str],
) -> Result<Vec<RenderedTemplate>> {
let new_type = new_test_framework.map(test_framework_type_entry);
let mut templates = vec![];
let base_tsconfig_path = base_path.join("tsconfig.base.json");
if base_tsconfig_path.exists() {
let content = read_to_string(&base_tsconfig_path)
.with_context(|| "Failed to read tsconfig.base.json")?;
let mut tsconfig: serde_json::Map<String, Value> =
serde_json::from_str(&content).with_context(|| "Failed to parse tsconfig.base.json")?;
swap_test_type_in_tsconfig(&mut tsconfig, new_type);
templates.push(RenderedTemplate {
path: base_tsconfig_path,
content: to_string_pretty(&tsconfig)?,
context: None,
});
}
for project_name in project_names {
let project_tsconfig_path = base_path.join(project_name).join("tsconfig.json");
if project_tsconfig_path.exists() {
let content = read_to_string(&project_tsconfig_path)
.with_context(|| format!("Failed to read tsconfig.json for {}", project_name))?;
let mut tsconfig: serde_json::Map<String, Value> = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse tsconfig.json for {}", project_name))?;
if tsconfig
.get("compilerOptions")
.and_then(|co| co.get("types"))
.is_some()
{
swap_test_type_in_tsconfig(&mut tsconfig, new_type);
templates.push(RenderedTemplate {
path: project_tsconfig_path,
content: to_string_pretty(&tsconfig)?,
context: None,
});
}
}
}
Ok(templates)
}