Skip to main content

code_baseline/
init.rs

1use std::path::Path;
2
3/// Detected project type based on config files present.
4#[derive(Debug, PartialEq)]
5pub enum ProjectType {
6    /// shadcn/ui + Tailwind CSS project (components.json + tailwind config)
7    ShadcnTailwind,
8    /// Tailwind CSS project (tailwind config but no shadcn)
9    TailwindOnly,
10    /// Generic JS/TS project (package.json but no Tailwind)
11    Generic,
12    /// Unknown project type
13    Unknown,
14}
15
16/// Detect the project type by checking for config files in the given directory.
17pub fn detect_project(dir: &Path) -> ProjectType {
18    let has_shadcn = dir.join("components.json").exists();
19    let has_tailwind = dir.join("tailwind.config.js").exists()
20        || dir.join("tailwind.config.ts").exists()
21        || dir.join("tailwind.config.mjs").exists()
22        || dir.join("tailwind.config.cjs").exists();
23    let has_package_json = dir.join("package.json").exists();
24
25    match (has_shadcn, has_tailwind, has_package_json) {
26        (true, _, _) => ProjectType::ShadcnTailwind,
27        (false, true, _) => ProjectType::TailwindOnly,
28        (false, false, true) => ProjectType::Generic,
29        _ => ProjectType::Unknown,
30    }
31}
32
33/// Generate a starter `baseline.toml` config for the detected project type.
34pub fn generate_config(project_type: &ProjectType) -> String {
35    match project_type {
36        ProjectType::ShadcnTailwind => generate_shadcn_config(),
37        ProjectType::TailwindOnly => generate_tailwind_config(),
38        ProjectType::Generic => generate_generic_config(),
39        ProjectType::Unknown => generate_generic_config(),
40    }
41}
42
43fn generate_shadcn_config() -> String {
44    r#"# baseline.toml — Baseline for shadcn/Tailwind
45# Generated by `baseline init` (shadcn + Tailwind detected)
46
47[baseline]
48name = "my-project"
49extends = ["shadcn-strict"]
50include = ["src/**/*", "app/**/*", "components/**/*"]
51exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]
52
53# The "shadcn-strict" preset includes these rules:
54#   enforce-dark-mode    (error)   — dark: variant required for color classes
55#   use-theme-tokens     (error)   — raw Tailwind colors → shadcn tokens
56#   no-inline-styles     (warning) — ban style={{ }}
57#   no-css-in-js         (error)   — ban styled-components, emotion
58#   no-competing-frameworks (error) — ban bootstrap, MUI, antd
59
60# Override a preset rule by redeclaring it with the same id:
61# [[rule]]
62# id = "use-theme-tokens"
63# type = "tailwind-theme-tokens"
64# severity = "warning"
65# glob = "**/*.{tsx,jsx}"
66# message = "Use shadcn semantic token instead of raw color"
67"#
68    .to_string()
69}
70
71fn generate_tailwind_config() -> String {
72    r#"# baseline.toml — Baseline for Tailwind CSS
73# Generated by `baseline init` (Tailwind detected)
74
75[baseline]
76name = "my-project"
77include = ["src/**/*", "app/**/*", "components/**/*"]
78exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]
79
80# ──────────────────────────────────────────────
81# Ban Inline Styles
82# Catch style={{ }} in JSX — use Tailwind classes instead.
83# ──────────────────────────────────────────────
84
85[[rule]]
86id = "no-inline-styles"
87type = "banned-pattern"
88severity = "warning"
89pattern = "style={{"
90glob = "**/*.{tsx,jsx}"
91message = "Avoid inline styles — use Tailwind utility classes instead"
92suggest = "Replace style={{ ... }} with Tailwind classes"
93
94# ──────────────────────────────────────────────
95# Ban CSS-in-JS Libraries
96# If you've committed to Tailwind, these shouldn't be imported.
97# ──────────────────────────────────────────────
98
99[[rule]]
100id = "no-css-in-js"
101type = "banned-import"
102severity = "error"
103packages = ["styled-components", "@emotion/styled", "@emotion/css", "@emotion/react"]
104message = "CSS-in-JS libraries conflict with Tailwind — use utility classes instead"
105
106# ──────────────────────────────────────────────
107# Ban Competing CSS Frameworks
108# Prevent mixing multiple CSS frameworks in the same project.
109# ──────────────────────────────────────────────
110
111[[rule]]
112id = "no-competing-frameworks"
113type = "banned-dependency"
114severity = "error"
115packages = ["bootstrap", "bulma", "@mui/material", "antd"]
116message = "Competing CSS framework detected — this project uses Tailwind"
117
118# ──────────────────────────────────────────────
119# Uncomment these rules if you add shadcn/ui:
120# ──────────────────────────────────────────────
121
122# [[rule]]
123# id = "enforce-dark-mode"
124# type = "tailwind-dark-mode"
125# severity = "error"
126# glob = "**/*.{tsx,jsx}"
127# message = "Missing dark: variant for color class"
128
129# [[rule]]
130# id = "use-theme-tokens"
131# type = "tailwind-theme-tokens"
132# severity = "warning"
133# glob = "**/*.{tsx,jsx}"
134# message = "Use shadcn semantic token instead of raw color"
135"#
136    .to_string()
137}
138
139fn generate_generic_config() -> String {
140    r#"# baseline.toml — Baseline configuration
141# Generated by `baseline init`
142#
143# Tip: If you use Tailwind CSS + shadcn/ui, add a components.json
144# and re-run `baseline init` for pre-configured theming rules.
145
146[baseline]
147name = "my-project"
148include = ["src/**/*", "app/**/*"]
149exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
150
151# ──────────────────────────────────────────────
152# Example: Ban a pattern
153# ──────────────────────────────────────────────
154
155# [[rule]]
156# id = "no-console-log"
157# type = "banned-pattern"
158# severity = "warning"
159# pattern = "console.log("
160# glob = "src/**/*.{ts,tsx}"
161# message = "Remove console.log before committing"
162
163# ──────────────────────────────────────────────
164# Example: Ban an import
165# ──────────────────────────────────────────────
166
167# [[rule]]
168# id = "no-moment"
169# type = "banned-import"
170# severity = "error"
171# packages = ["moment"]
172# message = "moment.js is deprecated — use date-fns or Temporal API"
173
174# ──────────────────────────────────────────────
175# Example: Ban a dependency
176# ──────────────────────────────────────────────
177
178# [[rule]]
179# id = "no-request"
180# type = "banned-dependency"
181# severity = "error"
182# packages = ["request"]
183# message = "The 'request' package is deprecated — use 'node-fetch'"
184"#
185    .to_string()
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::fs;
192
193    #[test]
194    fn detect_shadcn_project() {
195        let dir = tempfile::tempdir().unwrap();
196        fs::write(dir.path().join("components.json"), "{}").unwrap();
197        fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
198        fs::write(dir.path().join("package.json"), "{}").unwrap();
199        assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
200    }
201
202    #[test]
203    fn detect_shadcn_without_tailwind_config() {
204        let dir = tempfile::tempdir().unwrap();
205        fs::write(dir.path().join("components.json"), "{}").unwrap();
206        fs::write(dir.path().join("package.json"), "{}").unwrap();
207        // shadcn implies Tailwind even without explicit tailwind config
208        assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
209    }
210
211    #[test]
212    fn detect_tailwind_only() {
213        let dir = tempfile::tempdir().unwrap();
214        fs::write(dir.path().join("tailwind.config.js"), "").unwrap();
215        fs::write(dir.path().join("package.json"), "{}").unwrap();
216        assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
217    }
218
219    #[test]
220    fn detect_tailwind_ts_config() {
221        let dir = tempfile::tempdir().unwrap();
222        fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
223        assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
224    }
225
226    #[test]
227    fn detect_generic_project() {
228        let dir = tempfile::tempdir().unwrap();
229        fs::write(dir.path().join("package.json"), "{}").unwrap();
230        assert_eq!(detect_project(dir.path()), ProjectType::Generic);
231    }
232
233    #[test]
234    fn detect_unknown() {
235        let dir = tempfile::tempdir().unwrap();
236        assert_eq!(detect_project(dir.path()), ProjectType::Unknown);
237    }
238
239    #[test]
240    fn shadcn_config_uses_extends() {
241        let config = generate_config(&ProjectType::ShadcnTailwind);
242        assert!(config.contains(r#"extends = ["shadcn-strict"]"#));
243        // Should not contain inline rule definitions (only commented overrides)
244        assert!(!config.contains("\n[[rule]]"));
245    }
246
247    #[test]
248    fn tailwind_config_has_migration_rules() {
249        let config = generate_config(&ProjectType::TailwindOnly);
250        assert!(config.contains("banned-pattern"));
251        assert!(config.contains("banned-import"));
252        assert!(config.contains("banned-dependency"));
253        // Tailwind rules should be commented out
254        assert!(config.contains("# type = \"tailwind-dark-mode\""));
255    }
256
257    #[test]
258    fn generic_config_has_examples() {
259        let config = generate_config(&ProjectType::Generic);
260        assert!(config.contains("banned-pattern"));
261        assert!(config.contains("banned-import"));
262        assert!(config.contains("banned-dependency"));
263    }
264}