use std::path::Path;
#[derive(Debug, PartialEq)]
pub enum ProjectType {
ShadcnTailwind,
TailwindOnly,
Generic,
Unknown,
}
pub fn detect_project(dir: &Path) -> ProjectType {
let has_shadcn = dir.join("components.json").exists();
let has_tailwind = dir.join("tailwind.config.js").exists()
|| dir.join("tailwind.config.ts").exists()
|| dir.join("tailwind.config.mjs").exists()
|| dir.join("tailwind.config.cjs").exists();
let has_package_json = dir.join("package.json").exists();
match (has_shadcn, has_tailwind, has_package_json) {
(true, _, _) => ProjectType::ShadcnTailwind,
(false, true, _) => ProjectType::TailwindOnly,
(false, false, true) => ProjectType::Generic,
_ => ProjectType::Unknown,
}
}
pub fn generate_config(project_type: &ProjectType) -> String {
match project_type {
ProjectType::ShadcnTailwind => generate_shadcn_config(),
ProjectType::TailwindOnly => generate_tailwind_config(),
ProjectType::Generic => generate_generic_config(),
ProjectType::Unknown => generate_generic_config(),
}
}
fn generate_shadcn_config() -> String {
r#"# baseline.toml — Baseline for shadcn/Tailwind
# Generated by `baseline init` (shadcn + Tailwind detected)
[baseline]
name = "my-project"
extends = ["shadcn-strict"]
include = ["src/**/*", "app/**/*", "components/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]
# The "shadcn-strict" preset includes these rules:
# enforce-dark-mode (error) — dark: variant required for color classes
# use-theme-tokens (error) — raw Tailwind colors → shadcn tokens
# no-inline-styles (warning) — ban style={{ }}
# no-css-in-js (error) — ban styled-components, emotion
# no-competing-frameworks (error) — ban bootstrap, MUI, antd
# Override a preset rule by redeclaring it with the same id:
# [[rule]]
# id = "use-theme-tokens"
# type = "tailwind-theme-tokens"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# message = "Use shadcn semantic token instead of raw color"
"#
.to_string()
}
fn generate_tailwind_config() -> String {
r#"# baseline.toml — Baseline for Tailwind CSS
# Generated by `baseline init` (Tailwind detected)
[baseline]
name = "my-project"
include = ["src/**/*", "app/**/*", "components/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]
# ──────────────────────────────────────────────
# Ban Inline Styles
# Catch style={{ }} in JSX — use Tailwind classes instead.
# ──────────────────────────────────────────────
[[rule]]
id = "no-inline-styles"
type = "banned-pattern"
severity = "warning"
pattern = "style={{"
glob = "**/*.{tsx,jsx}"
message = "Avoid inline styles — use Tailwind utility classes instead"
suggest = "Replace style={{ ... }} with Tailwind classes"
# ──────────────────────────────────────────────
# Ban CSS-in-JS Libraries
# If you've committed to Tailwind, these shouldn't be imported.
# ──────────────────────────────────────────────
[[rule]]
id = "no-css-in-js"
type = "banned-import"
severity = "error"
packages = ["styled-components", "@emotion/styled", "@emotion/css", "@emotion/react"]
message = "CSS-in-JS libraries conflict with Tailwind — use utility classes instead"
# ──────────────────────────────────────────────
# Ban Competing CSS Frameworks
# Prevent mixing multiple CSS frameworks in the same project.
# ──────────────────────────────────────────────
[[rule]]
id = "no-competing-frameworks"
type = "banned-dependency"
severity = "error"
packages = ["bootstrap", "bulma", "@mui/material", "antd"]
message = "Competing CSS framework detected — this project uses Tailwind"
# ──────────────────────────────────────────────
# Uncomment these rules if you add shadcn/ui:
# ──────────────────────────────────────────────
# [[rule]]
# id = "enforce-dark-mode"
# type = "tailwind-dark-mode"
# severity = "error"
# glob = "**/*.{tsx,jsx}"
# message = "Missing dark: variant for color class"
# [[rule]]
# id = "use-theme-tokens"
# type = "tailwind-theme-tokens"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# message = "Use shadcn semantic token instead of raw color"
"#
.to_string()
}
fn generate_generic_config() -> String {
r#"# baseline.toml — Baseline configuration
# Generated by `baseline init`
#
# Tip: If you use Tailwind CSS + shadcn/ui, add a components.json
# and re-run `baseline init` for pre-configured theming rules.
[baseline]
name = "my-project"
include = ["src/**/*", "app/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
# ──────────────────────────────────────────────
# Example: Ban a pattern
# ──────────────────────────────────────────────
# [[rule]]
# id = "no-console-log"
# type = "banned-pattern"
# severity = "warning"
# pattern = "console.log("
# glob = "src/**/*.{ts,tsx}"
# message = "Remove console.log before committing"
# ──────────────────────────────────────────────
# Example: Ban an import
# ──────────────────────────────────────────────
# [[rule]]
# id = "no-moment"
# type = "banned-import"
# severity = "error"
# packages = ["moment"]
# message = "moment.js is deprecated — use date-fns or Temporal API"
# ──────────────────────────────────────────────
# Example: Ban a dependency
# ──────────────────────────────────────────────
# [[rule]]
# id = "no-request"
# type = "banned-dependency"
# severity = "error"
# packages = ["request"]
# message = "The 'request' package is deprecated — use 'node-fetch'"
"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn detect_shadcn_project() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("components.json"), "{}").unwrap();
fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
}
#[test]
fn detect_shadcn_without_tailwind_config() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("components.json"), "{}").unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
}
#[test]
fn detect_tailwind_only() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("tailwind.config.js"), "").unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
}
#[test]
fn detect_tailwind_ts_config() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
}
#[test]
fn detect_generic_project() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("package.json"), "{}").unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::Generic);
}
#[test]
fn detect_unknown() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(detect_project(dir.path()), ProjectType::Unknown);
}
#[test]
fn shadcn_config_uses_extends() {
let config = generate_config(&ProjectType::ShadcnTailwind);
assert!(config.contains(r#"extends = ["shadcn-strict"]"#));
assert!(!config.contains("\n[[rule]]"));
}
#[test]
fn tailwind_config_has_migration_rules() {
let config = generate_config(&ProjectType::TailwindOnly);
assert!(config.contains("banned-pattern"));
assert!(config.contains("banned-import"));
assert!(config.contains("banned-dependency"));
assert!(config.contains("# type = \"tailwind-dark-mode\""));
}
#[test]
fn generic_config_has_examples() {
let config = generate_config(&ProjectType::Generic);
assert!(config.contains("banned-pattern"));
assert!(config.contains("banned-import"));
assert!(config.contains("banned-dependency"));
}
}