use super::args::CliArgs;
use super::driver::{
CompilationCache, compile, compile_with_cache, compile_with_cache_and_changes,
};
use clap::Parser;
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use tsz_common::diagnostics::diagnostic_codes;
static TEMP_DIR_SEQUENCE: AtomicU64 = AtomicU64::new(0);
struct TempDir {
path: PathBuf,
}
impl TempDir {
fn new() -> std::io::Result<Self> {
let mut path = std::env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let seq = TEMP_DIR_SEQUENCE.fetch_add(1, Ordering::Relaxed);
path.push(format!(
"tsz_cli_driver_test_{}_{}_{}",
std::process::id(),
nanos,
seq
));
std::fs::create_dir_all(&path)?;
Ok(Self { path })
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn with_types_versions_env<T>(value: Option<&str>, f: impl FnOnce() -> T) -> T {
super::driver::with_types_versions_env(value, f)
}
fn write_file(path: &Path, contents: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("failed to create parent directory");
}
std::fs::write(path, contents).expect("failed to write file");
}
fn default_args() -> CliArgs {
CliArgs::try_parse_from(["tsz"]).expect("default args should parse")
}
#[test]
fn compile_with_tsconfig_emits_outputs() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
let args = default_args();
let result = with_types_versions_env(Some("5.9"), || {
compile(&args, base).expect("compile should succeed")
});
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/index.d.ts").is_file());
}
#[test]
fn compile_with_source_map_emits_map_outputs() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"sourceMap": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(result.diagnostics.is_empty());
let js_path = base.join("dist/src/index.js");
let map_path = base.join("dist/src/index.js.map");
assert!(js_path.is_file());
assert!(map_path.is_file());
let js_contents = std::fs::read_to_string(&js_path).expect("read js output");
assert!(js_contents.contains("sourceMappingURL=index.js.map"));
let map_contents = std::fs::read_to_string(&map_path).expect("read map output");
let map_json: Value = serde_json::from_str(&map_contents).expect("parse map json");
let file_field = map_json
.get("file")
.and_then(|value| value.as_str())
.unwrap_or("");
assert_eq!(file_field, "index.js");
let source_root = map_json
.get("sourceRoot")
.and_then(|value| value.as_str())
.unwrap_or("__missing__");
assert_eq!(source_root, "");
let sources_content = map_json
.get("sourcesContent")
.and_then(|value| value.as_array())
.expect("expected sourcesContent");
assert_eq!(sources_content.len(), 1);
assert_eq!(
sources_content[0].as_str().unwrap_or(""),
"export const value = 1;"
);
let mappings = map_json
.get("mappings")
.and_then(|value| value.as_str())
.unwrap_or("");
assert!(
mappings.contains(',') || mappings.contains(';'),
"expected non-trivial mappings, got: {mappings}"
);
}
#[test]
fn compile_with_declaration_map_emits_map_outputs() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(result.diagnostics.is_empty());
let dts_path = base.join("dist/src/index.d.ts");
let map_path = base.join("dist/src/index.d.ts.map");
assert!(dts_path.is_file());
assert!(map_path.is_file());
let dts_contents = std::fs::read_to_string(&dts_path).expect("read d.ts output");
assert!(dts_contents.contains("sourceMappingURL=index.d.ts.map"));
let map_contents = std::fs::read_to_string(&map_path).expect("read map output");
let map_json: Value = serde_json::from_str(&map_contents).expect("parse map json");
let file_field = map_json
.get("file")
.and_then(|value| value.as_str())
.unwrap_or("");
assert_eq!(file_field, "index.d.ts");
let source_root = map_json
.get("sourceRoot")
.and_then(|value| value.as_str())
.unwrap_or("__missing__");
assert_eq!(source_root, "");
let sources_content = map_json
.get("sourcesContent")
.and_then(|value| value.as_array())
.expect("expected sourcesContent");
assert_eq!(sources_content.len(), 1);
assert_eq!(
sources_content[0].as_str().unwrap_or(""),
"export const value = 1;"
);
let mappings = map_json
.get("mappings")
.and_then(|value| value.as_str())
.unwrap_or("");
assert!(
mappings.contains(',') || mappings.contains(';'),
"expected non-trivial mappings, got: {mappings}"
);
}
#[test]
fn compile_with_explicit_files_without_tsconfig() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(&base.join("main.ts"), "export const value = 1;");
let mut args = default_args();
args.files = vec![PathBuf::from("main.ts")];
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("main.js").is_file());
}
#[test]
fn compile_with_root_dir_flattens_output_paths() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/index.js").is_file());
assert!(base.join("dist/index.d.ts").is_file());
}
#[test]
fn compile_respects_no_emit_on_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "let x = ;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_with_project_dir_uses_tsconfig() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
let config_dir = base.join("configs");
write_file(
&config_dir.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&config_dir.join("src/index.ts"), "export const value = 1;");
let mut args = default_args();
args.project = Some(PathBuf::from("configs"));
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(config_dir.join("dist/src/index.js").is_file());
}
#[test]
fn compile_with_jsx_preserve_emits_jsx_extension() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"jsx": "preserve",
"strict": false
},
"include": ["src/**/*.tsx"]
}"#,
);
write_file(
&base.join("src/view.tsx"),
"export const View = () => <div />;",
);
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/src/view.jsx").is_file());
}
#[test]
fn compile_resolves_relative_imports_from_files_list() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from './util'; export { value };",
);
write_file(&base.join("src/util.ts"), "export const value = 1;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/util.js").is_file());
}
#[test]
fn compile_resolves_paths_mappings() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@lib/*": ["src/lib/*"]
}
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from '@lib/value'; export { value };",
);
write_file(&base.join("src/lib/value.ts"), "export const value = 1;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/src/lib/value.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from 'pkg'; export { value };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"types": "index.d.ts"
}"#,
);
write_file(
&base.join("node_modules/pkg/index.d.ts"),
"export const value = ;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/pkg/index.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_tsconfig_types_includes_selected_packages() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"types": ["foo"]
},
"files": ["src/index.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
write_file(
&base.join("node_modules/@types/foo/index.d.ts"),
"export const foo = ;",
);
write_file(
&base.join("node_modules/@types/bar/index.d.ts"),
"export const bar = ;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/@types/foo/index.d.ts"))
);
assert!(
!result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/@types/bar/index.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_tsconfig_type_roots_includes_packages() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typeRoots": ["types"]
},
"files": ["src/index.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
write_file(&base.join("types/foo/index.d.ts"), "export const foo = ;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/foo/index.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_exports_subpath() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"exports": {
".": { "types": "./types/index.d.ts" },
"./feature/*": { "types": "./types/feature/*.d.ts" }
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/feature/widget.d.ts"),
"export const widget = ;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
"*": {
"feature/*": ["types/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_best_match() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=6.1": {
"feature/*": ["types/v61/feature/*"]
},
">=5.0": {
"feature/*": ["types/v5/feature/*"]
},
"*": {
"feature/*": ["types/fallback/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v61/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/v5/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/fallback/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
if result.diagnostics.is_empty() {
assert!(base.join("dist/src/index.js").is_file());
} else {
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v5/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
}
#[test]
fn compile_resolves_node_modules_types_versions_prefers_specific_range() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=6.0": {
"feature/*": ["types/loose/feature/*"]
},
">=5.0 <7.0": {
"feature/*": ["types/ranged/feature/*"]
},
"*": {
"feature/*": ["types/fallback/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/loose/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/ranged/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/fallback/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/ranged/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_respects_cli_version_override() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let mut args = default_args();
args.types_versions_compiler_version = Some("7.1".to_string());
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v7/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_respects_env_version_override() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(Some("7.1"), || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v7/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_respects_tsconfig_version_override() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typesVersionsCompilerVersion": "7.1"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v7/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_tsconfig_extends_inherits_override() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("config/base.json"),
r#"{
"compilerOptions": {
"typesVersionsCompilerVersion": "7.1"
}
}"#,
);
write_file(
&base.join("tsconfig.json"),
r#"{
"extends": "./config/base.json",
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.1": {
"feature/*": ["types/v71/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v71/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v71/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_env_overrides_tsconfig() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typesVersionsCompilerVersion": "6.0"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(Some("7.1"), || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v7/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_empty_env_uses_tsconfig() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typesVersionsCompilerVersion": "7.1"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.1": {
"feature/*": ["types/v71/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v71/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = with_types_versions_env(Some(""), || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v71/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_cli_overrides_env_and_tsconfig() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typesVersionsCompilerVersion": "6.0"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.2": {
"feature/*": ["types/v72/feature/*"]
},
">=7.1": {
"feature/*": ["types/v71/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v72/feature/widget.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/types/v71/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = 1;",
);
let mut args = default_args();
args.types_versions_compiler_version = Some("7.2".to_string());
let result = with_types_versions_env(Some("7.1"), || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v72/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_invalid_override_falls_back() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = ;",
);
let mut args = default_args();
args.types_versions_compiler_version = Some("not-a-version".to_string());
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v6/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_invalid_env_falls_back() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = ;",
);
let args = default_args();
let result = with_types_versions_env(Some("not-a-version"), || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v6/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_invalid_tsconfig_falls_back() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true,
"typesVersionsCompilerVersion": "not-a-version"
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
">=6.0": {
"feature/*": ["types/v6/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/v6/feature/widget.d.ts"),
"export const widget = ;",
);
let args = default_args();
let result = with_types_versions_env(None, || {
compile(&args, base).expect("compile should succeed")
});
assert!(!result.diagnostics.is_empty());
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/v6/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_node_modules_types_versions_falls_back_to_wildcard() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg/feature/widget'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"typesVersions": {
">=7.0": {
"feature/*": ["types/v7/feature/*"]
},
"*": {
"feature/*": ["types/fallback/feature/*"]
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/types/v7/feature/widget.d.ts"),
"export const widget = 1;",
);
write_file(
&base.join("node_modules/pkg/types/fallback/feature/widget.d.ts"),
"export const widget = ;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
if result.diagnostics.is_empty() {
assert!(base.join("dist/src/index.js").is_file());
} else {
assert!(result.diagnostics.iter().any(|diag| {
diag.file
.contains("node_modules/pkg/types/fallback/feature/widget.d.ts")
}));
assert!(!base.join("dist/src/index.js").is_file());
}
}
#[test]
fn compile_resolves_package_imports_wildcard() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from '#utils/widget'; export { widget };",
);
write_file(
&base.join("package.json"),
r##"{
"imports": {
"#utils/*": "./types/*"
}
}"##,
);
write_file(&base.join("types/widget.d.ts"), "export const widget = ;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/widget.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_package_imports_prefers_types_condition() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { feature } from '#feature'; export { feature };",
);
write_file(
&base.join("package.json"),
r##"{
"imports": {
"#feature": {
"types": "./types/feature.d.ts",
"default": "./default/feature.d.ts"
}
}
}"##,
);
write_file(&base.join("types/feature.d.ts"), "export const feature = ;");
write_file(
&base.join("default/feature.d.ts"),
"export const feature = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/feature.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_package_imports_prefers_require_condition_for_commonjs() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { feature } from '#feature'; export { feature };",
);
write_file(
&base.join("package.json"),
r##"{
"imports": {
"#feature": {
"require": "./types/require.d.ts",
"import": "./types/import.d.ts"
}
}
}"##,
);
write_file(&base.join("types/require.d.ts"), "export const feature = ;");
write_file(&base.join("types/import.d.ts"), "export const feature = 1;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/require.d.ts"))
);
assert!(
!result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/import.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_resolves_package_imports_prefers_import_condition_for_esm() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { feature } from '#feature'; export { feature };",
);
write_file(
&base.join("package.json"),
r##"{
"imports": {
"#feature": {
"import": "./types/import.d.ts",
"require": "./types/require.d.ts"
}
}
}"##,
);
write_file(&base.join("types/import.d.ts"), "export const feature = ;");
write_file(
&base.join("types/require.d.ts"),
"export const feature = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/import.d.ts"))
);
assert!(
!result
.diagnostics
.iter()
.any(|diag| diag.file.contains("types/require.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_prefers_browser_exports_for_bundler() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"moduleResolution": "bundler",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { widget } from 'pkg'; export { widget };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"exports": {
".": {
"browser": "./browser.d.ts",
"node": "./node.d.ts"
}
}
}"#,
);
write_file(
&base.join("node_modules/pkg/browser.d.ts"),
"export const widget = ;",
);
write_file(
&base.join("node_modules/pkg/node.d.ts"),
"export const widget = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/pkg/browser.d.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_node_next_resolves_js_extension_to_ts() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"moduleResolution": "nodenext",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from './util.js'; export { value };",
);
write_file(&base.join("src/util.ts"), "export const value = ;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("src/util.ts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_node_next_prefers_mts_for_module_package() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"moduleResolution": "nodenext",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from 'pkg'; export { value };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"type": "module"
}"#,
);
write_file(
&base.join("node_modules/pkg/index.mts"),
"export const value = ;",
);
write_file(
&base.join("node_modules/pkg/index.cts"),
"export const value = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/pkg/index.mts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_node_next_prefers_cts_for_commonjs_package() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"moduleResolution": "nodenext",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
"import { value } from 'pkg'; export { value };",
);
write_file(
&base.join("node_modules/pkg/package.json"),
r#"{
"type": "commonjs"
}"#,
);
write_file(
&base.join("node_modules/pkg/index.mts"),
"export const value = 1;",
);
write_file(
&base.join("node_modules/pkg/index.cts"),
"export const value = ;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("node_modules/pkg/index.cts"))
);
assert!(!base.join("dist/src/index.js").is_file());
}
#[test]
fn compile_with_cache_emits_only_dirty_files() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/alpha.ts", "src/beta.ts"]
}"#,
);
let alpha_path = base.join("src/alpha.ts");
let beta_path = base.join("src/beta.ts");
write_file(&alpha_path, "export const alpha = 1;");
write_file(&beta_path, "export const beta = 2;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
let alpha_output = std::fs::canonicalize(base.join("dist/src/alpha.js"))
.unwrap_or_else(|_| base.join("dist/src/alpha.js"));
let beta_output = std::fs::canonicalize(base.join("dist/src/beta.js"))
.unwrap_or_else(|_| base.join("dist/src/beta.js"));
assert_eq!(result.emitted_files.len(), 2);
assert!(result.emitted_files.contains(&alpha_output));
assert!(result.emitted_files.contains(&beta_output));
write_file(&alpha_path, "export const alpha = 2;");
let canonical = std::fs::canonicalize(&alpha_path).unwrap_or(alpha_path);
cache.invalidate_paths_with_dependents(vec![canonical]);
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert_eq!(result.emitted_files.len(), 1);
assert!(result.emitted_files.contains(&alpha_output));
assert!(!result.emitted_files.contains(&beta_output));
}
#[test]
fn compile_with_cache_updates_dependencies_for_changed_files() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
let extra_path = base.join("src/extra.ts");
write_file(
&index_path,
"import { value } from './util'; export { value };",
);
write_file(&util_path, "export const value = ;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("util.ts"))
);
write_file(
&index_path,
"import { value } from './extra'; export { value };",
);
write_file(&extra_path, "export const value = ;");
let canonical = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let result = compile_with_cache_and_changes(&args, base, &mut cache, &[canonical])
.expect("compile should succeed");
assert!(
result
.diagnostics
.iter()
.any(|diag| diag.file.contains("extra.ts"))
);
assert!(
!result
.diagnostics
.iter()
.any(|diag| diag.file.contains("util.ts"))
);
}
#[test]
fn compile_with_cache_skips_dependents_when_exports_unchanged() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"import { value } from './util'; export const output = value;",
);
write_file(&util_path, "export function value() { return 1; }");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"initial diagnostics (unchanged exports): {:#?}",
result.diagnostics
);
write_file(&util_path, "export function value() { return 2; }");
let util_output = std::fs::canonicalize(base.join("dist/src/util.js"))
.unwrap_or_else(|_| base.join("dist/src/util.js"));
let index_output = std::fs::canonicalize(base.join("dist/src/index.js"))
.unwrap_or_else(|_| base.join("dist/src/index.js"));
let canonical = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let result = compile_with_cache_and_changes(&args, base, &mut cache, &[canonical])
.expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(result.emitted_files.contains(&util_output));
assert!(!result.emitted_files.contains(&index_output));
}
#[test]
fn compile_with_cache_rechecks_dependents_on_export_change() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"import * as util from './util'; export { util };",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"initial diagnostics (export change): {:#?}",
result.diagnostics
);
write_file(&util_path, "export const value = \"changed\";");
let util_output = std::fs::canonicalize(base.join("dist/src/util.js"))
.unwrap_or_else(|_| base.join("dist/src/util.js"));
let index_output = std::fs::canonicalize(base.join("dist/src/index.js"))
.unwrap_or_else(|_| base.join("dist/src/index.js"));
let canonical = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let result = compile_with_cache_and_changes(&args, base, &mut cache, &[canonical])
.expect("compile should succeed");
assert!(result.emitted_files.contains(&util_output));
assert!(result.emitted_files.contains(&index_output));
}
#[test]
fn compile_with_cache_invalidates_paths() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
write_file(&index_path, "export const value = ;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert_eq!(cache.len(), 1);
assert_eq!(cache.bind_len(), 1);
assert_eq!(cache.diagnostics_len(), 1);
let canonical = std::fs::canonicalize(&index_path).unwrap_or(index_path);
cache.invalidate_paths_with_dependents(vec![canonical]);
assert_eq!(cache.len(), 0);
assert_eq!(cache.bind_len(), 0);
assert_eq!(cache.diagnostics_len(), 0);
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert_eq!(cache.len(), 1);
assert_eq!(cache.bind_len(), 1);
assert_eq!(cache.diagnostics_len(), 1);
}
#[test]
fn compile_with_cache_invalidates_dependents() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"import { value } from './util'; export { value };",
);
write_file(&util_path, "export const value = ;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert_eq!(cache.len(), 2);
assert_eq!(cache.bind_len(), 2);
assert_eq!(cache.diagnostics_len(), 2);
let canonical = std::fs::canonicalize(&util_path).unwrap_or(util_path);
cache.invalidate_paths_with_dependents(vec![canonical]);
assert_eq!(cache.len(), 0);
assert_eq!(cache.bind_len(), 0);
assert_eq!(cache.diagnostics_len(), 0);
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(!result.diagnostics.is_empty());
assert_eq!(cache.len(), 2);
assert_eq!(cache.bind_len(), 2);
assert_eq!(cache.diagnostics_len(), 2);
}
#[test]
fn invalidate_paths_with_dependents_symbols_keeps_unrelated_cache() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"import { value } from './util'; export const local = 1; export const uses = value;",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
let canonical_index = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let canonical_util = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let before = cache.symbol_cache_len(&canonical_index).unwrap_or(0);
assert!(before > 0);
cache.invalidate_paths_with_dependents_symbols(vec![canonical_util.clone()]);
let after = cache.symbol_cache_len(&canonical_index).unwrap_or(0);
assert!(after > 0);
assert!(after < before);
assert_eq!(cache.node_cache_len(&canonical_index).unwrap_or(0), 0);
assert!(cache.symbol_cache_len(&canonical_util).is_none());
}
#[test]
fn invalidate_paths_with_dependents_symbols_handles_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"export { value } from './util'; export const local = 1;",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert_eq!(cache.len(), 2);
let canonical_index = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let canonical_util = std::fs::canonicalize(&util_path).unwrap_or(util_path);
cache.invalidate_paths_with_dependents_symbols(vec![canonical_util.clone()]);
assert_eq!(cache.len(), 1);
assert!(cache.symbol_cache_len(&canonical_index).is_some());
assert_eq!(cache.node_cache_len(&canonical_index).unwrap_or(1), 0);
assert!(cache.symbol_cache_len(&canonical_util).is_none());
}
#[test]
fn invalidate_paths_with_dependents_symbols_handles_import_equals() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"import util = require('./util'); export const local = util.value;",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Compilation should have no diagnostics, got: {:?}",
result.diagnostics
);
assert_eq!(cache.len(), 2);
let canonical_index = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let canonical_util = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let before_nodes = cache.node_cache_len(&canonical_index).unwrap_or(0);
assert!(before_nodes > 0);
cache.invalidate_paths_with_dependents_symbols(vec![canonical_util.clone()]);
assert_eq!(cache.len(), 1);
assert!(cache.symbol_cache_len(&canonical_index).is_some());
assert_eq!(cache.node_cache_len(&canonical_index).unwrap_or(1), 0);
assert!(cache.symbol_cache_len(&canonical_util).is_none());
}
#[test]
fn invalidate_paths_with_dependents_symbols_handles_namespace_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"export * as util from './util'; export const local = 1;",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert_eq!(cache.len(), 2);
let canonical_index = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let canonical_util = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let before_nodes = cache.node_cache_len(&canonical_index).unwrap_or(0);
assert!(before_nodes > 0);
cache.invalidate_paths_with_dependents_symbols(vec![canonical_util.clone()]);
assert_eq!(cache.len(), 1);
assert!(cache.symbol_cache_len(&canonical_index).is_some());
assert_eq!(cache.node_cache_len(&canonical_index).unwrap_or(1), 0);
assert!(cache.symbol_cache_len(&canonical_util).is_none());
}
#[test]
fn invalidate_paths_with_dependents_symbols_handles_star_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/index.ts"]
}"#,
);
let index_path = base.join("src/index.ts");
let util_path = base.join("src/util.ts");
write_file(
&index_path,
"export * from './util'; export const local = 1;",
);
write_file(&util_path, "export const value = 1;");
let mut cache = CompilationCache::default();
let args = default_args();
let result = compile_with_cache(&args, base, &mut cache).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert_eq!(cache.len(), 2);
let canonical_index = std::fs::canonicalize(&index_path).unwrap_or(index_path);
let canonical_util = std::fs::canonicalize(&util_path).unwrap_or(util_path);
let before_nodes = cache.node_cache_len(&canonical_index).unwrap_or(0);
assert!(before_nodes > 0);
cache.invalidate_paths_with_dependents_symbols(vec![canonical_util.clone()]);
assert_eq!(cache.len(), 1);
assert!(cache.symbol_cache_len(&canonical_index).is_some());
assert_eq!(cache.node_cache_len(&canonical_index).unwrap_or(1), 0);
assert!(cache.symbol_cache_len(&canonical_util).is_none());
}
#[test]
fn compile_multi_file_project_with_imports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "commonjs",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/models/user.ts"),
r#"
export interface User {
id: number;
name: string;
email: string;
}
export class UserImpl implements User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
getDisplayName(): string {
return this.name + " <" + this.email + ">";
}
}
export type UserId = number;
"#,
);
write_file(
&base.join("src/utils/helpers.ts"),
r#"
export function formatName(first: string, last: string): string {
return first + " " + last;
}
export function validateEmail(email: string): boolean {
return email.length > 0;
}
export const DEFAULT_PAGE_SIZE = 20;
"#,
);
write_file(
&base.join("src/services/user-service.ts"),
r#"
import * as models from '../models/user';
import * as helpers from '../utils/helpers';
export class UserService {
private users: models.User[] = [];
createUser(id: number, firstName: string, lastName: string, email: string): models.User | null {
if (!helpers.validateEmail(email)) {
return null;
}
const name = helpers.formatName(firstName, lastName);
const user = new models.UserImpl(id, name, email);
this.users.push(user);
return user;
}
getUserCount(): number {
return this.users.length;
}
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
// Re-export all from each module
export * from './models/user';
export * from './utils/helpers';
export * from './services/user-service';
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics, got: {:?}",
result.diagnostics
);
assert!(
base.join("dist/models/user.js").is_file(),
"models/user.js should exist"
);
assert!(
base.join("dist/models/user.d.ts").is_file(),
"models/user.d.ts should exist"
);
assert!(
base.join("dist/models/user.js.map").is_file(),
"models/user.js.map should exist"
);
assert!(
base.join("dist/utils/helpers.js").is_file(),
"utils/helpers.js should exist"
);
assert!(
base.join("dist/utils/helpers.d.ts").is_file(),
"utils/helpers.d.ts should exist"
);
assert!(
base.join("dist/services/user-service.js").is_file(),
"services/user-service.js should exist"
);
assert!(
base.join("dist/services/user-service.d.ts").is_file(),
"services/user-service.d.ts should exist"
);
assert!(
base.join("dist/index.js").is_file(),
"index.js should exist"
);
assert!(
base.join("dist/index.d.ts").is_file(),
"index.d.ts should exist"
);
let service_js = std::fs::read_to_string(base.join("dist/services/user-service.js"))
.expect("read service js");
assert!(
service_js.contains("require(") || service_js.contains("import"),
"Service JS should have require or import statements: {service_js}"
);
assert!(
service_js.contains("../models/user") || service_js.contains("./models/user"),
"Service JS should reference models/user: {service_js}"
);
assert!(
service_js.contains("../utils/helpers") || service_js.contains("./utils/helpers"),
"Service JS should reference utils/helpers: {service_js}"
);
let index_js = std::fs::read_to_string(base.join("dist/index.js")).expect("read index js");
assert!(
index_js.contains("exports")
&& (index_js.contains("require(") || index_js.contains("Object.defineProperty")),
"Index JS should have CommonJS exports: {index_js}"
);
let index_dts = std::fs::read_to_string(base.join("dist/index.d.ts")).expect("read index d.ts");
assert!(
index_dts.contains("export *") && index_dts.contains("./models/user"),
"Index d.ts should have re-export statements: {index_dts}"
);
let service_map_contents =
std::fs::read_to_string(base.join("dist/services/user-service.js.map"))
.expect("read service map");
let service_map: Value =
serde_json::from_str(&service_map_contents).expect("parse service map json");
let sources = service_map
.get("sources")
.and_then(|v| v.as_array())
.expect("sources array");
assert!(!sources.is_empty(), "Source map should have sources");
let sources_content = service_map.get("sourcesContent").and_then(|v| v.as_array());
assert!(
sources_content.is_some(),
"Source map should have sourcesContent"
);
}
#[test]
fn compile_multi_file_project_with_default_and_named_imports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "commonjs",
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/constants.ts"),
r#"
const CONFIG = {
apiUrl: "https://api.example.com",
timeout: 5000
};
export default CONFIG;
export const VERSION = "1.0.0";
"#,
);
write_file(
&base.join("src/math.ts"),
r#"
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const PI = 3.14159;
"#,
);
write_file(
&base.join("src/app.ts"),
r#"
// Default import
import CONFIG from './constants';
// Named import alongside default
import { VERSION } from './constants';
// Named imports with alias
import { add as addNumbers, multiply, PI } from './math';
export function runApp(): string {
const sum = addNumbers(1, 2);
const product = multiply(3, 4);
const circumference = 2 * PI * 10;
const url = CONFIG.apiUrl;
return url + " v" + VERSION + " sum=" + sum + " product=" + product + " circ=" + circumference;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics, got: {:?}",
result.diagnostics
);
assert!(base.join("dist/constants.js").is_file());
assert!(base.join("dist/math.js").is_file());
assert!(base.join("dist/app.js").is_file());
assert!(base.join("dist/app.d.ts").is_file());
let app_js = std::fs::read_to_string(base.join("dist/app.js")).expect("read app js");
assert!(
app_js.contains("./constants") || app_js.contains("constants"),
"App JS should reference constants: {app_js}"
);
assert!(
app_js.contains("./math") || app_js.contains("math"),
"App JS should reference math: {app_js}"
);
let app_dts = std::fs::read_to_string(base.join("dist/app.d.ts")).expect("read app d.ts");
assert!(
app_dts.contains("runApp"),
"App d.ts should export runApp: {app_dts}"
);
}
#[test]
fn compile_multi_file_project_with_type_imports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/types.ts"),
r#"
export interface Logger {
log(msg: string): void;
}
export type LogLevel = "debug" | "info" | "error";
"#,
);
write_file(
&base.join("src/logger.ts"),
r#"
import type { Logger, LogLevel } from './types';
export class ConsoleLogger implements Logger {
private level: LogLevel;
constructor(level: LogLevel) {
this.level = level;
}
log(msg: string): void {
// log implementation
}
getLevel(): LogLevel {
return this.level;
}
}
export function createLogger(level: LogLevel): Logger {
return new ConsoleLogger(level);
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export type { Logger, LogLevel } from './types';
export { ConsoleLogger, createLogger } from './logger';
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Type imports should compile without errors: {:?}",
result.diagnostics
);
assert!(base.join("dist/src/types.js").is_file());
assert!(base.join("dist/src/logger.js").is_file());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/index.d.ts").is_file());
let index_dts =
std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read index d.ts");
assert!(
index_dts.contains("Logger") && index_dts.contains("LogLevel"),
"Index d.ts should have type exports for Logger and LogLevel: {index_dts}"
);
let logger_js =
std::fs::read_to_string(base.join("dist/src/logger.js")).expect("read logger js");
assert!(
logger_js.contains("ConsoleLogger") && logger_js.contains("createLogger"),
"Logger JS should have class and function exports: {logger_js}"
);
}
#[test]
fn compile_declaration_true_emits_dts_files() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export const VERSION = "1.0.0";
export function greet(name: string): string {
return "Hello, " + name;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics, got: {:?}",
result.diagnostics
);
assert!(
base.join("dist/src/index.js").is_file(),
"JS output should exist"
);
assert!(
base.join("dist/src/index.d.ts").is_file(),
"Declaration file should exist when declaration: true"
);
let dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read d.ts");
assert!(
dts.contains("VERSION") && dts.contains("string"),
"Declaration should contain VERSION: {dts}"
);
assert!(
dts.contains("greet") && dts.contains("name"),
"Declaration should contain greet function: {dts}"
);
}
#[test]
fn compile_declaration_false_no_dts_files() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": false
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("dist/src/index.js").is_file(),
"JS output should exist"
);
assert!(
!base.join("dist/src/index.d.ts").is_file(),
"Declaration file should NOT exist when declaration: false"
);
}
#[test]
fn compile_declaration_absent_no_dts_files() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("dist/src/index.js").is_file(),
"JS output should exist"
);
assert!(
!base.join("dist/src/index.d.ts").is_file(),
"Declaration file should NOT exist when declaration is not specified"
);
}
#[test]
fn compile_declaration_interface_and_type() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/types.ts"),
r#"
export interface User {
id: number;
name: string;
email: string;
}
export type UserId = number;
export type UserRole = "admin" | "user" | "guest";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
let dts_path = base.join("dist/src/types.d.ts");
assert!(dts_path.is_file(), "Declaration file should exist");
let dts = std::fs::read_to_string(&dts_path).expect("read d.ts");
assert!(
dts.contains("interface User"),
"Declaration should contain User interface: {dts}"
);
assert!(
dts.contains("id") && dts.contains("number"),
"Declaration should contain id property: {dts}"
);
assert!(
dts.contains("name") && dts.contains("string"),
"Declaration should contain name property: {dts}"
);
assert!(
dts.contains("UserId"),
"Declaration should contain UserId type: {dts}"
);
assert!(
dts.contains("UserRole"),
"Declaration should contain UserRole type: {dts}"
);
}
#[test]
fn compile_declaration_class_with_methods() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/calculator.ts"),
r#"
export class Calculator {
private value: number;
constructor(initial: number) {
this.value = initial;
}
add(n: number): void {
this.value = this.value + n;
}
subtract(n: number): void {
this.value = this.value - n;
}
getResult(): number {
return this.value;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics, got: {:?}",
result.diagnostics
);
let dts_path = base.join("dist/src/calculator.d.ts");
assert!(dts_path.is_file(), "Declaration file should exist");
let dts = std::fs::read_to_string(&dts_path).expect("read d.ts");
assert!(
dts.contains("class Calculator"),
"Declaration should contain Calculator class: {dts}"
);
assert!(
dts.contains("add") && dts.contains("void"),
"Declaration should contain add method with void return: {dts}"
);
assert!(
dts.contains("subtract") && dts.contains("void"),
"Declaration should contain subtract method with void return: {dts}"
);
assert!(
dts.contains("getResult") && dts.contains("number"),
"Declaration should contain getResult method: {dts}"
);
assert!(
dts.contains("private") && dts.contains("value"),
"Declaration should contain private value: {dts}"
);
}
#[test]
fn compile_declaration_with_declaration_dir() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationDir": "types"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("dist/index.js").is_file(),
"JS output should be in dist/"
);
assert!(
base.join("types/index.d.ts").is_file(),
"Declaration file should be in types/"
);
assert!(
!base.join("dist/index.d.ts").is_file(),
"Declaration file should NOT be in dist/ when declarationDir is set"
);
}
#[test]
fn compile_outdir_places_output_in_directory() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "build"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("build/src/index.js").is_file(),
"JS output should be in build/src/"
);
assert!(
!base.join("src/index.js").is_file(),
"JS output should NOT be alongside source when outDir is set"
);
}
#[test]
fn compile_outdir_absent_outputs_alongside_source() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("src/index.js").is_file(),
"JS output should be alongside source when outDir is not set"
);
}
#[test]
fn compile_outdir_with_rootdir_flattens_paths() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
write_file(
&base.join("src/utils/helpers.ts"),
"export const helper = 1;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("dist/index.js").is_file(),
"JS output should be at dist/index.js (flattened)"
);
assert!(
base.join("dist/utils/helpers.js").is_file(),
"Nested JS output should be at dist/utils/helpers.js"
);
assert!(
!base.join("dist/src/index.js").is_file(),
"Output should NOT include src/ when rootDir is set to src"
);
}
#[test]
fn compile_outdir_nested_structure() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const main = 1;");
write_file(&base.join("src/models/user.ts"), "export const user = 2;");
write_file(
&base.join("src/utils/helpers.ts"),
"export const helper = 3;",
);
write_file(
&base.join("src/services/api/client.ts"),
"export const client = 4;",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/models/user.js").is_file());
assert!(base.join("dist/src/utils/helpers.js").is_file());
assert!(base.join("dist/src/services/api/client.js").is_file());
}
#[test]
fn compile_outdir_deep_nested_path() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "build/output/js"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("build/output/js/src/index.js").is_file(),
"JS output should be in build/output/js/src/"
);
}
#[test]
fn compile_outdir_with_declaration_and_sourcemap() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 42;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(
base.join("dist/index.js").is_file(),
"JS should be in outDir"
);
assert!(
base.join("dist/index.d.ts").is_file(),
"Declaration should be in outDir"
);
assert!(
base.join("dist/index.js.map").is_file(),
"Source map should be in outDir"
);
let map_contents = std::fs::read_to_string(base.join("dist/index.js.map")).expect("read map");
let map_json: Value = serde_json::from_str(&map_contents).expect("parse map");
let file_field = map_json.get("file").and_then(|v| v.as_str()).unwrap_or("");
assert_eq!(
file_field, "index.js",
"Source map file field should be index.js"
);
}
#[test]
fn compile_outdir_multiple_entry_points() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/main.ts"), "export const main = 1;");
write_file(&base.join("src/worker.ts"), "export const worker = 2;");
write_file(&base.join("src/cli.ts"), "export const cli = 3;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("dist/main.js").is_file());
assert!(base.join("dist/worker.js").is_file());
assert!(base.join("dist/cli.js").is_file());
}
#[test]
fn compile_missing_file_in_files_array_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/missing.ts"]
}"#,
);
let args = default_args();
let result = compile(&args, base);
assert!(result.is_err(), "Should return error for missing file");
let err = result.unwrap_err().to_string();
assert!(
err.contains("file not found") || err.contains("not found") || err.contains("missing"),
"Error should mention file not found: {err}"
);
assert!(!base.join("dist").is_dir());
}
#[test]
fn compile_missing_file_in_include_pattern_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
let args = default_args();
let result = compile(&args, base);
let compilation = result.expect("Should return Ok with diagnostics, not a fatal error");
assert!(
compilation.diagnostics.iter().any(|d| d.code == 18003),
"Should contain TS18003 diagnostic when no input files found, got: {:?}",
compilation
.diagnostics
.iter()
.map(|d| d.code)
.collect::<Vec<_>>()
);
}
#[test]
fn compile_missing_file_in_include_pattern_reports_custom_config_path() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
let config_rel = PathBuf::from("configs/custom-name.json");
let config_path = base.join(&config_rel);
write_file(
&config_path,
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
let mut args = default_args();
args.project = Some(config_rel);
let compilation = compile(&args, base).expect("Should return Ok with diagnostics");
let ts18003 = compilation
.diagnostics
.iter()
.find(|d| d.code == 18003)
.expect("expected TS18003 diagnostic");
let expected_path = config_path.canonicalize().expect("canonical config path");
let expected_path = expected_path.to_string_lossy();
assert!(
ts18003.message_text.contains(expected_path.as_ref()),
"TS18003 should include resolved config path: {}",
ts18003.message_text
);
}
#[test]
fn compile_missing_file_in_include_pattern_prefers_ts18003_over_type_root_diagnostics() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"target": "es2015"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}"#,
);
write_file(
&base.join("node_modules/@types/lib-extender/index.d.ts"),
r#"declare var lib: () => void;
declare namespace lib {}
export = lib;
declare module "lib" {
export function fn(): void;
}"#,
);
let args = default_args();
let compilation = compile(&args, base).expect("Should return Ok with diagnostics");
assert!(
compilation.diagnostics.iter().any(|d| d.code == 18003),
"Expected TS18003 when include pattern has no matching source files"
);
assert!(
!compilation.diagnostics.iter().any(|d| d.code == 2649),
"TS2649 from @types files should not be reported when there are no root inputs"
);
}
#[test]
fn compile_missing_single_file_via_cli_args_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
let mut args = default_args();
args.files = vec![PathBuf::from("nonexistent.ts")];
let result = compile(&args, base);
assert!(result.is_err(), "Should return error for missing CLI file");
let err = result.unwrap_err().to_string();
assert!(
err.contains("not found") || err.contains("No such file"),
"Error should mention file not found: {err}"
);
}
#[test]
fn compile_missing_multiple_files_in_files_array_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"files": ["src/a.ts", "src/b.ts", "src/c.ts"]
}"#,
);
write_file(&base.join("src/b.ts"), "export const b = 2;");
let args = default_args();
let result = compile(&args, base);
assert!(
result.is_err(),
"Should return error when some files in files array are missing"
);
}
#[test]
fn compile_missing_project_directory_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
let mut args = default_args();
args.project = Some(PathBuf::from("nonexistent_project"));
let result = compile(&args, base).expect("compile should succeed with error diagnostic");
assert!(
!result.diagnostics.is_empty(),
"Should have error diagnostic for missing project directory"
);
assert_eq!(
result.diagnostics[0].code,
diagnostic_codes::CANNOT_FIND_A_TSCONFIG_JSON_FILE_AT_THE_SPECIFIED_DIRECTORY,
"Should have correct error code"
);
}
#[test]
fn compile_missing_tsconfig_in_project_dir_returns_error() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
std::fs::create_dir_all(base.join("myproject")).expect("create dir");
write_file(&base.join("myproject/index.ts"), "export const value = 42;");
let mut args = default_args();
args.project = Some(PathBuf::from("myproject"));
let result = compile(&args, base).expect("compile should succeed with error diagnostic");
assert!(
!result.diagnostics.is_empty(),
"Should have error diagnostic when tsconfig.json is missing in project dir"
);
assert_eq!(
result.diagnostics[0].code,
diagnostic_codes::CANNOT_FIND_A_TSCONFIG_JSON_FILE_AT_THE_SPECIFIED_DIRECTORY,
"Should have correct error code"
);
}
#[test]
fn compile_missing_tsconfig_uses_defaults() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(&base.join("src/index.ts"), "export const value = 42;");
let mut args = default_args();
args.files = vec![PathBuf::from("src/index.ts")];
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
assert!(base.join("src/index.js").is_file());
}
#[test]
fn compile_ambient_external_module_without_internal_import_declaration_fixture() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"module": "commonjs"
},
"files": [
"ambientExternalModuleWithoutInternalImportDeclaration_0.ts",
"ambientExternalModuleWithoutInternalImportDeclaration_1.ts"
]
}"#,
);
write_file(
&base.join("ambientExternalModuleWithoutInternalImportDeclaration_0.ts"),
r#"declare module 'M' {
namespace C {
export var f: number;
}
class C {
foo(): void;
}
export = C;
}"#,
);
write_file(
&base.join("ambientExternalModuleWithoutInternalImportDeclaration_1.ts"),
r#"/// <reference path='ambientExternalModuleWithoutInternalImportDeclaration_0.ts'/>
import A = require('M');
var c = new A();"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics, got {:?}",
result
.diagnostics
.iter()
.map(|d| d.code)
.collect::<Vec<_>>()
);
}
#[test]
fn compile_alias_on_merged_module_interface_fixture_reports_ts2708() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"module": "commonjs"
},
"files": [
"aliasOnMergedModuleInterface_0.ts",
"aliasOnMergedModuleInterface_1.ts"
]
}"#,
);
write_file(
&base.join("aliasOnMergedModuleInterface_0.ts"),
r#"declare module "foo" {
namespace B {
export interface A {}
}
interface B {
bar(name: string): B.A;
}
export = B;
}"#,
);
write_file(
&base.join("aliasOnMergedModuleInterface_1.ts"),
r#"/// <reference path='aliasOnMergedModuleInterface_0.ts' />
import foo = require("foo");
declare var z: foo;
z.bar("hello");
var x: foo.A = foo.bar("hello");"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.iter().any(|d| d.code == 2708),
"Expected TS2708, got {:?}",
result
.diagnostics
.iter()
.map(|d| d.code)
.collect::<Vec<_>>()
);
}
#[test]
fn compile_generic_utility_library_array_utils() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"strict": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/array.ts"),
r#"
export function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(fn(arr[i], i));
}
return result;
}
export function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
export function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
for (const item of arr) {
if (predicate(item)) {
return item;
}
}
return undefined;
}
export function reduce<T, U>(arr: T[], fn: (acc: U, item: T) => U, initial: U): U {
let acc = initial;
for (const item of arr) {
acc = fn(acc, item);
}
return acc;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
assert!(
base.join("dist/src/array.js").is_file(),
"JS output should exist"
);
assert!(
base.join("dist/src/array.d.ts").is_file(),
"Declaration should exist"
);
let js = std::fs::read_to_string(base.join("dist/src/array.js")).expect("read js");
assert!(!js.contains(": T[]"), "Type annotations should be stripped");
assert!(!js.contains(": U[]"), "Type annotations should be stripped");
assert!(js.contains("function map"), "Function should be present");
assert!(js.contains("function filter"), "Function should be present");
assert!(js.contains("function find"), "Function should be present");
assert!(js.contains("function reduce"), "Function should be present");
let dts = std::fs::read_to_string(base.join("dist/src/array.d.ts")).expect("read dts");
assert!(
dts.contains("map<T, U>") || dts.contains("map<T,U>"),
"Generic should be in declaration"
);
assert!(
dts.contains("filter<T>"),
"Generic should be in declaration"
);
}
#[test]
fn compile_generic_utility_library_type_utilities() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/types.ts"),
r#"
// Note: Object, Readonly, Partial are provided by lib.d.ts
// Type-level utilities (erased at runtime)
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? Readonly<T[P]> : T[P];
};
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? Partial<T[P]> : T[P];
};
export type Nullable<T> = T | null;
// Mapped type that uses index access (T[P])
export type ValueTypes<T> = {
[P in keyof T]: T[P];
};
// Runtime function using these types
export function deepFreeze<T extends object>(obj: T): DeepReadonly<T> {
Object.freeze(obj);
for (const key of Object.keys(obj)) {
const value = (obj as Record<string, unknown>)[key];
if (typeof value === "object" && value !== null) {
deepFreeze(value as object);
}
}
return obj as DeepReadonly<T>;
}
export function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Compilation should have no diagnostics, got: {:?}",
result.diagnostics
);
assert!(
base.join("dist/src/types.js").is_file(),
"JS output should exist"
);
assert!(
base.join("dist/src/types.d.ts").is_file(),
"Declaration should exist"
);
let js = std::fs::read_to_string(base.join("dist/src/types.js")).expect("read js");
assert!(!js.contains("DeepReadonly"), "Type alias should be erased");
assert!(!js.contains("DeepPartial"), "Type alias should be erased");
assert!(
js.contains("function deepFreeze"),
"Runtime function should be present"
);
assert!(
js.contains("function isNonNull"),
"Runtime function should be present"
);
let dts = std::fs::read_to_string(base.join("dist/src/types.d.ts")).expect("read dts");
assert!(
dts.contains("DeepReadonly"),
"Type alias should be in declaration"
);
assert!(
dts.contains("DeepPartial"),
"Type alias should be in declaration"
);
}
#[test]
fn compile_generic_utility_library_multi_file() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/array.ts"),
r#"
export function first<T>(arr: T[]): T | undefined {
return arr[0];
}
export function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
"#,
);
write_file(
&base.join("src/string.ts"),
r#"
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function repeat(str: string, count: number): string {
let result = "";
for (let i = 0; i < count; i++) {
result += str;
}
return result;
}
"#,
);
write_file(
&base.join("src/function.ts"),
r#"
export function identity<T>(value: T): T {
return value;
}
export function constant<T>(value: T): () => T {
return () => value;
}
export function noop(): void {}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { first, last } from "./array";
export { capitalize, repeat } from "./string";
export { identity, constant, noop } from "./function";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
assert!(base.join("dist/src/array.js").is_file());
assert!(base.join("dist/src/string.js").is_file());
assert!(base.join("dist/src/function.js").is_file());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/array.d.ts").is_file());
assert!(base.join("dist/src/string.d.ts").is_file());
assert!(base.join("dist/src/function.d.ts").is_file());
assert!(base.join("dist/src/index.d.ts").is_file());
assert!(base.join("dist/src/array.js.map").is_file());
assert!(base.join("dist/src/index.js.map").is_file());
let index_js = std::fs::read_to_string(base.join("dist/src/index.js")).expect("read index");
assert!(
index_js.contains("require") || index_js.contains("export"),
"Index should have exports"
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("first") && index_dts.contains("last"),
"Index declaration should re-export array utils"
);
}
#[test]
fn compile_generic_utility_library_with_constraints() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/constrained.ts"),
r#"
// Generic with extends constraint
export function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Generic with multiple constraints
export function setProperty<T extends object, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): T {
obj[key] = value;
return obj;
}
// Generic with default type parameter
export function createArray<T = string>(length: number, fill: T): T[] {
const result: T[] = [];
for (let i = 0; i < length; i++) {
result.push(fill);
}
return result;
}
// Function overloads with generics
export function wrap<T>(value: T): T[];
export function wrap<T>(value: T, count: number): T[];
export function wrap<T>(value: T, count: number = 1): T[] {
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(value);
}
return result;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/constrained.js")).expect("read js");
assert!(
!js.contains("extends keyof"),
"Constraints should be stripped"
);
assert!(
!js.contains("extends object"),
"Constraints should be stripped"
);
assert!(
js.contains("function getProperty"),
"Function should be present"
);
assert!(js.contains("function wrap"), "Function should be present");
let dts = std::fs::read_to_string(base.join("dist/src/constrained.d.ts")).expect("read dts");
assert!(
dts.contains("getProperty"),
"getProperty should be in declaration"
);
assert!(
dts.contains("setProperty"),
"setProperty should be in declaration"
);
assert!(
dts.contains("createArray"),
"createArray should be in declaration"
);
assert!(dts.contains("wrap"), "wrap should be in declaration");
}
#[test]
fn compile_generic_utility_library_classes() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/collections.ts"),
r#"
export class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
export class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
front(): T | undefined {
return this.items[0];
}
get size(): number {
return this.items.length;
}
}
export class Result<T, E> {
private constructor(
private readonly value: T | undefined,
private readonly error: E | undefined,
private readonly isOk: boolean
) {}
static ok<T, E>(value: T): Result<T, E> {
return new Result<T, E>(value, undefined, true);
}
static err<T, E>(error: E): Result<T, E> {
return new Result<T, E>(undefined, error, false);
}
isSuccess(): boolean {
return this.isOk;
}
getValue(): T | undefined {
return this.value;
}
getError(): E | undefined {
return this.error;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/collections.js")).expect("read js");
assert!(js.contains("class Stack"), "Class should be present");
assert!(js.contains("class Queue"), "Class should be present");
assert!(js.contains("class Result"), "Class should be present");
assert!(!js.contains("<T>"), "Generic parameters should be stripped");
assert!(!js.contains("T[]"), "Type annotations should be stripped");
assert!(
!js.contains(": void"),
"Return type annotations should be stripped"
);
let dts = std::fs::read_to_string(base.join("dist/src/collections.d.ts")).expect("read dts");
assert!(
dts.contains("Stack<T>"),
"Generic class should be in declaration"
);
assert!(
dts.contains("Queue<T>"),
"Generic class should be in declaration"
);
assert!(
dts.contains("Result<T, E>") || dts.contains("Result<T,E>"),
"Generic class should be in declaration"
);
}
#[test]
fn compile_module_named_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/utils.ts"),
r#"
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const PI = 3.14159;
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { add, multiply, PI } from "./utils";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
assert!(base.join("dist/src/utils.js").is_file());
assert!(base.join("dist/src/index.js").is_file());
assert!(base.join("dist/src/index.d.ts").is_file());
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(index_dts.contains("add"), "add should be re-exported");
assert!(
index_dts.contains("multiply"),
"multiply should be re-exported"
);
assert!(index_dts.contains("PI"), "PI should be re-exported");
}
#[test]
fn compile_module_renamed_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/internal.ts"),
r#"
export function internalHelper(): string {
return "helper";
}
export const internalValue = 42;
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { internalHelper as helper, internalValue as value } from "./internal";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(index_dts.contains("helper"), "helper should be re-exported");
assert!(index_dts.contains("value"), "value should be re-exported");
}
#[test]
fn compile_module_star_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/math.ts"),
r#"
export function sum(arr: number[]): number {
let total = 0;
for (const n of arr) {
total += n;
}
return total;
}
export function average(arr: number[]): number {
return sum(arr) / arr.length;
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export * from "./math";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("sum") || index_dts.contains("*"),
"sum should be re-exported or star export present"
);
}
#[test]
fn compile_module_chained_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/core.ts"),
r#"
export function coreFunction(): string {
return "core";
}
export const CORE_VERSION = "1.0.0";
"#,
);
write_file(
&base.join("src/intermediate.ts"),
r#"
export { coreFunction, CORE_VERSION } from "./core";
export function intermediateFunction(): string {
return "intermediate";
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { coreFunction, CORE_VERSION, intermediateFunction } from "./intermediate";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
assert!(base.join("dist/src/core.js").is_file());
assert!(base.join("dist/src/intermediate.js").is_file());
assert!(base.join("dist/src/index.js").is_file());
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("coreFunction"),
"coreFunction should be re-exported"
);
assert!(
index_dts.contains("intermediateFunction"),
"intermediateFunction should be re-exported"
);
}
#[test]
fn compile_module_mixed_exports_and_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/helpers.ts"),
r#"
export function helperA(): string {
return "A";
}
export function helperB(): string {
return "B";
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
// Re-exports
export { helperA, helperB } from "./helpers";
// Local exports
export function localFunction(): number {
return 42;
}
export const LOCAL_CONSTANT = "local";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let index_js = std::fs::read_to_string(base.join("dist/src/index.js")).expect("read js");
assert!(
index_js.contains("localFunction"),
"Local function should be in output"
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("helperA"),
"helperA should be re-exported"
);
assert!(
index_dts.contains("localFunction"),
"localFunction should be exported"
);
assert!(
index_dts.contains("LOCAL_CONSTANT"),
"LOCAL_CONSTANT should be exported"
);
}
#[test]
fn compile_module_type_only_reexports() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/types.ts"),
r#"
export type UserId = number;
export type UserName = string;
export function createId(n: number): UserId {
return n;
}
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
// Type-only re-exports (should be erased from JS)
export type { UserId, UserName } from "./types";
// Value re-export
export { createId } from "./types";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let index_js = std::fs::read_to_string(base.join("dist/src/index.js")).expect("read js");
assert!(
index_js.contains("createId"),
"createId should be in output"
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("UserId"),
"UserId type should be in declaration"
);
assert!(
index_dts.contains("createId"),
"createId should be in declaration"
);
}
#[test]
fn compile_module_default_reexport() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/component.ts"),
r#"
export default function Component(): string {
return "Component";
}
export const version = "1.0";
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { default, version } from "./component";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(
index_dts.contains("default") || index_dts.contains("Component"),
"default export should be re-exported"
);
assert!(
index_dts.contains("version"),
"version should be re-exported"
);
}
#[test]
fn compile_module_barrel_file() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/features/auth.ts"),
r#"
export function login(user: string): boolean {
return user.length > 0;
}
export function logout(): void {}
"#,
);
write_file(
&base.join("src/features/data.ts"),
r#"
export function fetchData(): string[] {
return [];
}
export function saveData(data: string[]): boolean {
return data.length > 0;
}
"#,
);
write_file(
&base.join("src/features/index.ts"),
r#"
export { login, logout } from "./auth";
export { fetchData, saveData } from "./data";
"#,
);
write_file(
&base.join("src/index.ts"),
r#"
export { login, logout, fetchData, saveData } from "./features";
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
assert!(base.join("dist/src/features/auth.js").is_file());
assert!(base.join("dist/src/features/data.js").is_file());
assert!(base.join("dist/src/features/index.js").is_file());
assert!(base.join("dist/src/index.js").is_file());
let index_dts = std::fs::read_to_string(base.join("dist/src/index.d.ts")).expect("read dts");
assert!(index_dts.contains("login"), "login should be re-exported");
assert!(
index_dts.contains("fetchData"),
"fetchData should be re-exported"
);
}
#[test]
fn compile_class_with_generic_constructor() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/builder.ts"),
r#"
export class Builder<T> {
private value: T;
constructor(initial: T) {
this.value = initial;
}
set(value: T): Builder<T> {
this.value = value;
return this;
}
transform<U>(fn: (value: T) => U): Builder<U> {
return new Builder(fn(this.value));
}
build(): T {
return this.value;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/builder.js")).expect("read js");
assert!(js.contains("class Builder"), "Class should be present");
assert!(js.contains("constructor("), "Constructor should be present");
assert!(!js.contains("<T>"), "Generic should be stripped");
let dts = std::fs::read_to_string(base.join("dist/src/builder.d.ts")).expect("read dts");
assert!(
dts.contains("Builder<T>"),
"Generic class should be in declaration"
);
assert!(
dts.contains("transform<U>"),
"Generic method should be in declaration"
);
}
#[test]
fn compile_basic_namespace_export() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/utils.ts"),
r#"
export namespace Utils {
export const VERSION = "1.0.0";
export function greet(name: string): string {
return "Hello, " + name;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/utils.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_nested_namespace_export() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/api.ts"),
r#"
export namespace API {
export namespace V1 {
export function getUsers(): string[] {
return ["user1", "user2"];
}
}
export namespace V2 {
export function getUsers(): string[] {
return ["user1", "user2", "user3"];
}
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/api.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_namespace_with_class() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/models.ts"),
r#"
export namespace Models {
export class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/models.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_numeric_enum() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/status.ts"),
r#"
export enum Status {
Pending,
Active,
Completed,
Failed
}
export function getStatusName(status: Status): string {
return Status[status];
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/status.js")).expect("read js");
assert!(js.contains("Status"), "Enum should be present in JS");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_string_enum() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/direction.ts"),
r#"
export enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
export function move(dir: Direction): Direction {
return dir;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/direction.js")).expect("read js");
assert!(js.contains("Direction"), "Enum should be present in JS");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_const_enum() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/flags.ts"),
r#"
export const enum Flags {
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
export function hasFlag(flags: Flags, flag: Flags): boolean {
return (flags & flag) !== 0;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/flags.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_enum_with_computed_values() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/sizes.ts"),
r#"
export enum Size {
Small = 1,
Medium = Small * 2,
Large = Medium * 2,
ExtraLarge = Large * 2
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/sizes.js")).expect("read js");
assert!(js.contains("Size"), "Enum should be present in JS");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_basic_arrow_function() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/utils.ts"),
r#"
export const add = (a: number, b: number): number => a + b;
export const multiply = (a: number, b: number): number => {
return a * b;
};
export const identity = <T>(x: T): T => x;
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/utils.js")).expect("read js");
assert!(
js.contains("=>") || js.contains("function"),
"Arrow or function should be present"
);
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_arrow_function_with_rest_params() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/helpers.ts"),
r#"
export const sum = (...numbers: number[]): number => {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
};
export const first = <T>(...items: T[]): T => items[0];
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/helpers.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_arrow_function_with_default_params() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/greet.ts"),
r#"
export const greet = (name: string, greeting: string = "Hello"): string => {
return greeting + ", " + name;
};
export const repeat = (str: string, times: number = 1): string => {
let result = "";
for (let i = 0; i < times; i++) {
result += str;
}
return result;
};
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/greet.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_arrow_function_in_class() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/counter.ts"),
r#"
export class Counter {
count: number = 0;
increment = (): void => {
this.count++;
};
decrement = (): void => {
this.count--;
};
reset = (): void => {
this.count = 0;
};
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/counter.js")).expect("read js");
assert!(js.contains("Counter"), "Class should be present");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_array_spread() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/arrays.ts"),
r#"
export function concat(a: number[], b: number[]): number[] {
return [...a, ...b];
}
export function copy(a: number[]): number[] {
return [...a];
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/arrays.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_object_spread() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/objects.ts"),
r#"
interface Person {
name: string;
age: number;
}
export function clone(obj: Person): Person {
return { ...obj };
}
export function update(obj: Person, updates: Person): Person {
return { ...obj, ...updates };
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/objects.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_function_call_spread() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/calls.ts"),
r#"
export function apply(fn: (...args: number[]) => number, args: number[]): number {
return fn(...args);
}
export function log(...items: string[]): string[] {
return items;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/calls.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_basic_template_literal() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/greet.ts"),
r#"
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function format(a: number, b: number): string {
return `${a} + ${b} = ${a + b}`;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/greet.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_multiline_template_literal() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/html.ts"),
r#"
export function createDiv(content: string): string {
const result = `<div><p>${content}</p></div>`;
return result;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/html.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_nested_template_literal() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/nested.ts"),
r#"
export function wrap(inner: string, outer: string): string {
return `${outer}: ${`[${inner}]`}`;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/nested.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_object_destructuring() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/extract.ts"),
r#"
interface Point {
x: number;
y: number;
}
export function getX(point: Point): number {
const { x } = point;
return x;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/extract.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_array_destructuring() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/arrays.ts"),
r#"
export function getFirst(arr: number[]): number {
const [first] = arr;
return first;
}
export function getSecond(arr: number[]): number {
const [, second] = arr;
return second;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/arrays.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_destructuring_with_defaults() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/defaults.ts"),
r#"
interface Config {
host: string;
port: number;
}
export function getPort(config: Config): number {
const { port = 3000 } = config;
return port;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/defaults.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_optional_chaining() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/optional.ts"),
r#"
interface User {
name: string;
address?: {
city: string;
};
}
export function getCity(user: User): string | undefined {
return user.address?.city;
}
export function getLength(arr?: string[]): number | undefined {
return arr?.length;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/optional.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_nullish_coalescing() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/nullish.ts"),
r#"
export function getValueOrDefault(value: string | null | undefined): string {
return value ?? "default";
}
export function getNumberOrZero(num: number | null): number {
return num ?? 0;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/nullish.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_optional_chaining_with_call() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/optcall.ts"),
r#"
interface Logger {
log?: (msg: string) => void;
}
export function maybeLog(logger: Logger, msg: string): void {
logger.log?.(msg);
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/optcall.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_class_inheritance() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/classes.ts"),
r#"
export class Animal {
constructor(public name: string) {}
speak(): string {
return this.name;
}
}
export class Dog extends Animal {
constructor(name: string) {
super(name);
}
speak(): string {
return "Woof: " + super.speak();
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/classes.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_class_static_members() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/staticclass.ts"),
r#"
export class Counter {
static count: number = 0;
static increment(): number {
Counter.count += 1;
return Counter.count;
}
static reset(): void {
Counter.count = 0;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/staticclass.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_class_accessors() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/accessors.ts"),
r#"
export class Rectangle {
private _width: number = 0;
private _height: number = 0;
get width(): number {
return this._width;
}
set width(value: number) {
this._width = value;
}
get area(): number {
return this._width * this._height;
}
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/accessors.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_computed_property_names() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/computed.ts"),
r#"
const KEY = "dynamicKey";
export const obj = {
[KEY]: "value",
["literal" + "Key"]: 42
};
export function getProp(key: string): { [k: string]: number } {
return { [key]: 100 };
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/computed.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_for_of_loop() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/forof.ts"),
r#"
export function sumArray(arr: number[]): number {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum;
}
export function joinStrings(arr: string[]): string {
let result = "";
for (const str of arr) {
result += str;
}
return result;
}
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/forof.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_shorthand_methods() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/methods.ts"),
r#"
export const calculator = {
add(a: number, b: number): number {
return a + b;
},
subtract(a: number, b: number): number {
return a - b;
}
};
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(
result.diagnostics.is_empty(),
"Should compile without errors: {:?}",
result.diagnostics
);
let js = std::fs::read_to_string(base.join("dist/src/methods.js")).expect("read js");
assert!(!js.is_empty(), "JS output should not be empty");
}
#[test]
fn compile_incremental_creates_tsbuildinfo() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"outDir": "dist",
"incremental": true,
"tsBuildInfoFile": "dist/project.tsbuildinfo"
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(&base.join("src/index.ts"), "export const value = 1;");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
assert!(result.diagnostics.is_empty());
let js_path = base.join("dist/src/index.js");
assert!(js_path.is_file(), "JS output should exist");
let build_info_path = base.join("dist/project.tsbuildinfo");
assert!(
build_info_path.is_file(),
"tsbuildinfo file should be created"
);
let build_info_content = std::fs::read_to_string(&build_info_path).expect("read buildinfo");
let build_info: serde_json::Value =
serde_json::from_str(&build_info_content).expect("parse buildinfo");
assert_eq!(
build_info["version"],
crate::incremental::BUILD_INFO_VERSION
);
assert!(build_info["rootFiles"].is_array());
let result2 = compile(&args, base).expect("second compile should succeed");
assert!(result2.diagnostics.is_empty());
let build_info_content2 =
std::fs::read_to_string(&build_info_path).expect("read buildinfo again");
let build_info2: serde_json::Value =
serde_json::from_str(&build_info_content2).expect("parse buildinfo again");
assert_eq!(
build_info2["version"],
crate::incremental::BUILD_INFO_VERSION
);
write_file(
&base.join("src/index.ts"),
"export const value = 2; export const foo = 'bar';",
);
let result3 = compile(&args, base).expect("third compile should succeed");
assert!(result3.diagnostics.is_empty());
let build_info_content3 =
std::fs::read_to_string(&build_info_path).expect("read buildinfo third time");
let build_info3: serde_json::Value =
serde_json::from_str(&build_info_content3).expect("parse buildinfo third time");
assert_eq!(
build_info3["version"],
crate::incremental::BUILD_INFO_VERSION
);
}
use crate::driver::has_no_types_and_symbols_directive;
#[test]
fn test_has_no_types_and_symbols_directive_true() {
let source = r#"// @noTypesAndSymbols: true
async function f(x, y = z) {}"#;
assert!(has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_false() {
let source = r#"// @noTypesAndSymbols: false
async function f(x, y = z) {}"#;
assert!(!has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_not_present() {
let source = r#"// @strict: true
async function f(x, y = z) {}"#;
assert!(!has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_case_insensitive() {
let source = r#"// @NOTYPESANDSYMBOLS: true
async function f(x, y = z) {}"#;
assert!(has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_with_other_options() {
let source = r#"// @strict: false
// @target: es2015
// @noTypesAndSymbols: true
async function f(x, y = z) {}"#;
assert!(has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_comma_separated() {
let source = r#"// @noTypesAndSymbols: true, false
async function f(x, y = z) {}"#;
assert!(has_no_types_and_symbols_directive(source));
}
#[test]
fn test_has_no_types_and_symbols_directive_after_32_lines() {
let source = format!("{}\n// @noTypesAndSymbols: true", "\n".repeat(35));
assert!(!has_no_types_and_symbols_directive(&source));
}
#[test]
fn test_has_no_types_and_symbols_directive_semicolon_terminated() {
let source = r#"// @noTypesAndSymbols: true;
async function f(x, y = z) {}"#;
assert!(has_no_types_and_symbols_directive(source));
}
#[test]
fn test_no_types_and_symbols_directive_does_not_disable_default_libs() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"noEmit": true
},
"include": ["src/**/*.ts"]
}"#,
);
write_file(
&base.join("src/index.ts"),
r#"// @noTypesAndSymbols: true
const value = 1;
"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
let ts2318_errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == diagnostic_codes::CANNOT_FIND_GLOBAL_TYPE)
.collect();
assert!(
ts2318_errors.is_empty(),
"Expected @noTypesAndSymbols not to disable libs, got TS2318 diagnostics: {:?}",
ts2318_errors
.iter()
.map(|d| d.message_text.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn compile_binary_file_reports_errors() {
let temp = TempDir::new().expect("temp dir");
let base = &temp.path;
let binary_path = base.join("binary.ts");
let content = b"G@\xFFG@\xFFG@";
std::fs::write(&binary_path, content).expect("failed to write binary file");
write_file(
&base.join("tsconfig.json"),
r#"{
"compilerOptions": {
"target": "es2015"
},
"files": ["binary.ts"]
}"#,
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
let has_ts1490 = result.diagnostics.iter().any(|d| d.code == 1490);
assert!(
has_ts1490,
"Expected TS1490 (File appears to be binary). Diagnostics: {:?}",
result.diagnostics
);
let non_binary_errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code != 1490)
.collect();
assert!(
non_binary_errors.is_empty(),
"Expected only TS1490 for binary files, but got additional errors: {non_binary_errors:?}"
);
}
#[test]
fn ts2688_unresolved_types_in_tsconfig() {
let tmp = TempDir::new().unwrap();
let base = &tmp.path;
std::fs::create_dir_all(base.join("node_modules/@types")).unwrap();
write_file(
&base.join("tsconfig.json"),
r#"{ "compilerOptions": { "types": ["nonexistent-package"] }, "files": ["index.ts"] }"#,
);
write_file(&base.join("index.ts"), "const x: number = 1;\n");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
let ts2688_diags: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == diagnostic_codes::CANNOT_FIND_TYPE_DEFINITION_FILE_FOR)
.collect();
assert!(
!ts2688_diags.is_empty(),
"Expected TS2688 for unresolved 'nonexistent-package' in types array, got codes: {:?}",
result
.diagnostics
.iter()
.map(|d| d.code)
.collect::<Vec<_>>()
);
assert!(
ts2688_diags[0].message_text.contains("nonexistent-package"),
"TS2688 message should mention the package name, got: {}",
ts2688_diags[0].message_text
);
}
#[test]
fn ts2688_resolved_types_no_error() {
let tmp = TempDir::new().unwrap();
let base = &tmp.path;
write_file(
&base.join("node_modules/@types/mylib/index.d.ts"),
"declare const myLibValue: string;\n",
);
write_file(
&base.join("tsconfig.json"),
r#"{ "compilerOptions": { "types": ["mylib"] }, "files": ["index.ts"] }"#,
);
write_file(&base.join("index.ts"), "const x: number = 1;\n");
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
let ts2688_diags: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == diagnostic_codes::CANNOT_FIND_TYPE_DEFINITION_FILE_FOR)
.collect();
assert!(
ts2688_diags.is_empty(),
"Should NOT emit TS2688 when types package is found, got: {:?}",
ts2688_diags
);
}
#[test]
fn ts2307_emitted_for_commonjs_module() {
let tmp = TempDir::new().unwrap();
let base = &tmp.path;
write_file(
&base.join("tsconfig.json"),
r#"{ "compilerOptions": { "module": "commonjs" }, "files": ["test.ts"] }"#,
);
write_file(
&base.join("test.ts"),
"import { thing } from \"non-existent-module\";\nthing();\n",
);
let args = default_args();
let result = compile(&args, base).expect("compile should succeed");
let ts2307 = result.diagnostics.iter().any(|d| {
d.code == diagnostic_codes::CANNOT_FIND_MODULE_OR_ITS_CORRESPONDING_TYPE_DECLARATIONS
});
assert!(
ts2307,
"Expected TS2307 for bare specifier with module: commonjs, got codes: {:?}",
result
.diagnostics
.iter()
.map(|d| d.code)
.collect::<Vec<_>>()
);
}