Skip to main content

code_split_plugin_typescript/
lib.rs

1//! TypeScript language plugin for Code Split.
2//!
3//! Handles `.ts` and `.tsx` files via `tree-sitter-typescript`, reusing the
4//! shared ECMAScript walker/resolver from `code-split-plugin-javascript`.
5
6use 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
17/// The TypeScript language plugin (handles .ts / .tsx / .mts / .cts).
18pub 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            // Resolve imports TS-first, then JS fallbacks.
45            &["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// ─────────────────────────────────────────────────────────────────────────────
56// Tests
57// ─────────────────────────────────────────────────────────────────────────────
58
59#[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}