code_split_plugin_typescript/
lib.rs1use anyhow::Result;
7use code_split_plugin_api::{
8 graph::Graph,
9 level::Level,
10 plugin::{LanguagePlugin, PluginInput},
11};
12use code_split_plugin_javascript::{
13 analyze_ecmascript, detect_with_marker, ecmascript_is_test_path, ecmascript_level,
14};
15use std::path::Path;
16
17pub struct TypescriptPlugin;
19
20const TS_EXTS: &[&str] = &["ts", "tsx", "mts", "cts"];
21
22impl LanguagePlugin for TypescriptPlugin {
23 fn name(&self) -> &str {
24 "typescript"
25 }
26
27 fn detect(&self, workspace: &Path, _input: &PluginInput) -> bool {
28 detect_with_marker(workspace, "tsconfig.json")
29 }
30
31 fn levels(&self) -> Vec<Level> {
32 vec![ecmascript_level("files")]
33 }
34
35 fn analyze(&self, workspace: &Path, _level: &str, input: &PluginInput) -> Result<Graph> {
36 analyze_ecmascript(
37 workspace,
38 TS_EXTS,
39 |ext| match ext {
40 "ts" | "mts" | "cts" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
41 "tsx" => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
42 _ => None,
43 },
44 &["ts", "tsx", "mts", "cts", "js", "jsx"],
46 input.ignore_tests,
47 )
48 }
49
50 fn is_test_path(&self, rel_path: &str) -> bool {
51 ecmascript_is_test_path(rel_path)
52 }
53}
54
55#[cfg(test)]
60mod tests {
61 use super::*;
62 use code_split_plugin_api::plugin::LanguagePlugin;
63 use std::fs;
64 use tempfile::TempDir;
65
66 fn write_file(dir: &std::path::Path, rel: &str, contents: &str) {
67 let p = dir.join(rel);
68 fs::create_dir_all(p.parent().unwrap()).unwrap();
69 fs::write(p, contents).unwrap();
70 }
71
72 #[test]
73 fn plugin_name_is_typescript() {
74 assert_eq!(TypescriptPlugin.name(), "typescript");
75 }
76
77 #[test]
78 fn detect_requires_tsconfig() {
79 let tmp = TempDir::new().unwrap();
80 let input = PluginInput::default();
81 assert!(!TypescriptPlugin.detect(tmp.path(), &input));
82 fs::write(tmp.path().join("tsconfig.json"), "{}").unwrap();
83 assert!(TypescriptPlugin.detect(tmp.path(), &input));
84 }
85
86 #[test]
87 fn levels_returns_single_files_level() {
88 let levels = TypescriptPlugin.levels();
89 assert_eq!(levels.len(), 1);
90 assert_eq!(levels[0].name, "files");
91 assert!(levels[0].edge_kinds.contains_key("uses"));
92 }
93
94 #[test]
95 fn analyze_builds_ts_graph_with_imports_and_externals() {
96 let tmp = TempDir::new().unwrap();
97 let root = tmp.path();
98
99 write_file(
100 root,
101 "src/a.ts",
102 "import { greet } from \"./b\";\n\
103 import React from \"react\";\n\
104 export function helper() { return greet(); }\n",
105 );
106 write_file(
107 root,
108 "src/b.ts",
109 "export function greet(): string { return \"hi\"; }\n",
110 );
111
112 let input = PluginInput::default();
113 let graph = TypescriptPlugin
114 .analyze(root, "files", &input)
115 .expect("TypescriptPlugin.analyze should succeed");
116
117 let a_id = root.join("src/a.ts").to_string_lossy().into_owned();
118 let b_id = root.join("src/b.ts").to_string_lossy().into_owned();
119
120 assert!(
121 graph.nodes.iter().any(|n| n.id == a_id && n.kind == "file"),
122 "a.ts node present"
123 );
124 assert!(
125 graph
126 .edges
127 .iter()
128 .any(|e| e.source == a_id && e.target == b_id && e.kind == "uses"),
129 "expected import edge a.ts → b.ts"
130 );
131 assert!(
132 graph
133 .nodes
134 .iter()
135 .any(|n| n.id == "ext:react" && n.kind == "external"),
136 "external node for react"
137 );
138 }
139}