mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
6.5 KiB
6.5 KiB
Prowler UI - AI Agent Ruleset
CRITICAL RULES - NON-NEGOTIABLE
React
- ALWAYS:
import { useState, useEffect } from "react" - NEVER:
import React,import * as React,import React as * - NEVER:
useMemo,useCallback(React Compiler handles optimization)
Types
- ALWAYS:
const X = { A: "a", B: "b" } as const; type T = typeof X[keyof typeof X] - NEVER:
type T = "a" | "b"
Interfaces
- ALWAYS: One level depth only; object property → dedicated interface (recursive)
- ALWAYS: Reuse via
extends - NEVER: Inline nested objects
// ✅ CORRECT
interface UserAddress {
street: string;
city: string;
}
interface User {
id: string;
address: UserAddress;
}
interface Admin extends User {
permissions: string[];
}
// ❌ WRONG
interface User {
address: { street: string; city: string };
}
Styling
- Single class:
className="bg-slate-800 text-white" - Merge multiple classes:
className={cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")}(cn() handles Tailwind conflicts with twMerge) - Conditional classes:
className={cn("base", condition && "variant")} - Recharts props:
fill={CHART_COLORS.text}(use constants with var()) - Dynamic values:
style={{ width: "50%", opacity: 0.5 }} - CSS custom properties:
style={{ "--color": "var(--css-var)" }}(for dynamic theming) - NEVER:
var()in className strings (use Tailwind semantic classes instead) - NEVER: hex colors (use
text-whitenottext-[#fff])
Scope Rule (ABSOLUTE)
- Used 2+ places →
components/shared/orlib/ortypes/orhooks/ - Used 1 place → keep local in feature directory
- This determines ALL folder structure decisions
Memoization
- NEVER:
useMemo,useCallback - React 19 Compiler handles automatic optimization
DECISION TREES
Component Placement
New feature UI? → shadcn/ui + Tailwind | Existing feature? → HeroUI
Used 1 feature? → features/{feature}/components | Used 2+? → components/shared
Needs state/hooks? → "use client" | Server component? → No directive
Code Location
Server action → actions/{feature}/{feature}.ts
Data transform → actions/{feature}/{feature}.adapter.ts
Types (shared 2+) → types/{domain}.ts | Types (local 1) → {feature}/types.ts
Utils (shared 2+) → lib/ | Utils (local 1) → {feature}/utils/
Hooks (shared 2+) → hooks/ | Hooks (local 1) → {feature}/hooks.ts
shadcn components → components/shadcn/ | HeroUI → components/ui/
Styling Decision
Tailwind class exists? → className | Dynamic value? → style prop
Conditional styles? → cn() | Static? → className only
Recharts? → CHART_COLORS constant + var() | Other? → Tailwind classes
PATTERNS
Server Component
export default async function Page() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
Form + Validation
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const form = useForm({ resolver: zodResolver(schema) });
Server Action
"use server";
export async function updateProvider(formData: FormData) {
const validated = schema.parse(Object.fromEntries(formData));
await updateDB(validated);
revalidatePath("/path");
}
Zod v4
z.email()notz.string().email()z.uuid()notz.string().uuid()z.url()notz.string().url()z.string().min(1)notz.string().nonempty()errorparam notmessageparam
Zustand v5
const useStore = create(
persist(
(set) => ({
value: 0,
increment: () => set((s) => ({ value: s.value + 1 })),
}),
{ name: "key" },
),
);
AI SDK v5
import { useChat } from "@ai-sdk/react";
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const [input, setInput] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
};
Testing (Playwright)
export class FeaturePage extends BasePage {
readonly submitBtn = this.page.getByRole("button", { name: "Submit" });
async goto() {
await super.goto("/path");
}
async submit() {
await this.submitBtn.click();
}
}
test(
"action works",
{ tag: ["@critical", "@feature", "@TEST-001"] },
async ({ page }) => {
const p = new FeaturePage(page);
await p.goto();
await p.submit();
await expect(page).toHaveURL("/expected");
},
);
Selector priority: getByRole() → getByLabel() → getByText() → other
TECH STACK
Next.js 15.5.3 | React 19.1.1 | Tailwind 4.1.13 | shadcn/ui (new) | HeroUI 2.8.4 (legacy) Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.29 | Recharts 2.15.4
PROJECT STRUCTURE
ui/
├── app/ (Next.js App Router)
│ ├── (auth)/ (Auth pages)
│ └── (prowler)/ (Main app: compliance, findings, providers, scans, services, integrations)
├── components/
│ ├── shadcn/ (New shadcn/ui components)
│ ├── ui/ (HeroUI base)
│ └── {domain}/ (Domain components)
├── actions/ (Server actions)
├── types/ (Shared types)
├── hooks/ (Shared hooks)
├── lib/ (Utilities)
├── store/ (Zustand state)
├── tests/ (Playwright E2E)
└── styles/ (Global CSS)
COMMANDS
pnpm install && pnpm run dev (Setup & start)
pnpm run typecheck (Type check)
pnpm run lint:fix (Fix linting)
pnpm run format:write (Format)
pnpm run healthcheck (typecheck + lint)
pnpm run test:e2e (E2E tests)
pnpm run test:e2e:ui (E2E with UI)
pnpm run test:e2e:debug (Debug E2E)
pnpm run build && pnpm start (Build & start)
QA CHECKLIST BEFORE COMMIT
npm run typecheckpassesnpm run lint:fixpassesnpm run format:writepasses- Relevant E2E tests pass
- All UI states handled (loading, error, empty)
- No secrets in code (use
.env.local) - Error messages sanitized
- Server-side validation present
MIGRATIONS (As of Jan 2025)
React 18 → 19.1.1 (async components, compiler) Next.js 14 → 15.5.3 NextUI → HeroUI 2.8.4 Zod 3 → 4 (see patterns section) AI SDK 4 → 5 (see patterns section)