1use std::path::Path;
2
3#[derive(Debug, PartialEq)]
5pub enum ProjectType {
6 ShadcnTailwind,
8 TailwindOnly,
10 Generic,
12 Unknown,
14}
15
16pub 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
33pub 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 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 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 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}