From 524ffe4bada6a870045b407cbdd6645d2e1bba97 Mon Sep 17 00:00:00 2001 From: "Jhon Lloyd F. Omblero" Date: Mon, 3 Aug 2026 20:56:27 +0800 Subject: [PATCH] feat: rule tree refactor, anti-slop blocks, framework content, registry fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/types.ts: add RuleBlock, BlockInclusion, ResolvedBlock, ResolvedRuleTree - core/resolver.ts: 4-step pipeline — expand ancestors, deduplicate by canonical key (pack priority), build parent-child tree, topological sort - core/registry.ts: add getAllBlocks() + readBlock(block); fix double-blocks/ path bug that caused all block reads to return empty string - core/generator.ts: wire resolver pipeline; composeBlocks takes ResolvedRuleTree - cli/src/types.ts, resolver.ts, registry.ts, generator.ts: mirror all above changes for the Node.js CLI context Pack configs — migrate string[] blocks to typed RuleBlock[]: - packs/frontend/pack.config.ts: all blocks typed with id/tags/inclusion - packs/backend/pack.config.ts: all blocks typed - packs/fullstack/pack.config.ts: full union of frontend+backend blocks via canonical cross-pack id references (no content duplication); all 11 framework options; all optional blocks from both packs Framework content — replace stub bullet lists with real rule content: - packs/frontend/blocks/frameworks/nextjs.md: folder structure, RSC vs CC, Server Actions, data fetching, caching, Route Handlers - packs/frontend/blocks/frameworks/nuxt.md: useFetch/useAsyncData, Nitro routes, useState vs Pinia, NuxtImg/NuxtLink, middleware - packs/frontend/blocks/frameworks/sveltekit.md: server vs universal load, form actions + use:enhance, Svelte 5 runes, hooks, route protection - packs/frontend/blocks/frameworks/angular.md: standalone components, OnPush, signals, RxJS bridge, lazy routes, @defer, NgOptimizedImage - packs/frontend/blocks/frameworks/vanilla.md: new file — DOM helpers, class component pattern, store, event delegation, Web Components New blocks: - packs/frontend/blocks/anti-slop.md: what slop looks like, mandatory checks, spacing/typography/color/component discipline - packs/frontend/blocks/anti-slop-tooling.md: child of anti-slop — GSAP, Motion.dev, Rive, Lenis, shadcn/ui, Radix UI, Magic UI, Aceternity, Smooth UI, Raylight, Unlumen, Tasteskill.dev, Three.js/R3F; each entry has problem/when-to-reach/when-to-skip/minimal-usage/do-not structure - cli/src/resolver.ts: new file (mirrors core/resolver.ts, Node-compatible) UI fix: - src/components/Generator.tsx: fix optional blocks panel — was reading deprecated pack.optionalBlocks (always undefined); now reads pack.blocks.filter(b => b.inclusion === 'optional') correctly AGY skill structure: - .agent/skills/frontend-rules/SKILL.md: moved to AGY-compliant path with required YAML frontmatter Tests: - cli/tests/cli.test.ts: 10 new regression tests — Jaccard similarity checks (flags if two framework outputs share >75% word tokens) + keyword presence tests per framework --- .agent/skills/frontend-rules/SKILL.md | 544 ++++++++++++++++++ cli/SKILL.md | 374 ++++++++++++ cli/src/generator.ts | 59 +- cli/src/registry.ts | 19 +- cli/src/resolver.ts | 211 +++++++ cli/src/types.ts | 49 +- cli/tests/cli.test.ts | 132 +++++ core/generator.ts | 80 ++- core/registry.ts | 35 +- core/resolver.ts | 263 +++++++++ core/types.ts | 113 +++- packs/backend/pack.config.ts | 99 +++- packs/frontend/blocks/anti-slop-tooling.md | 450 +++++++++++++++ packs/frontend/blocks/anti-slop.md | 96 ++++ packs/frontend/blocks/frameworks/angular.md | 237 +++++++- packs/frontend/blocks/frameworks/nextjs.md | 189 +++++- packs/frontend/blocks/frameworks/nuxt.md | 180 +++++- packs/frontend/blocks/frameworks/sveltekit.md | 212 ++++++- packs/frontend/blocks/frameworks/vanilla.md | 227 ++++++++ packs/frontend/pack.config.ts | 166 +++++- packs/fullstack/pack.config.ts | 288 +++++++++- src/components/Generator.tsx | 20 +- 22 files changed, 3923 insertions(+), 120 deletions(-) create mode 100644 .agent/skills/frontend-rules/SKILL.md create mode 100644 cli/SKILL.md create mode 100644 cli/src/resolver.ts create mode 100644 core/resolver.ts create mode 100644 packs/frontend/blocks/anti-slop-tooling.md create mode 100644 packs/frontend/blocks/anti-slop.md create mode 100644 packs/frontend/blocks/frameworks/vanilla.md diff --git a/.agent/skills/frontend-rules/SKILL.md b/.agent/skills/frontend-rules/SKILL.md new file mode 100644 index 0000000..ce1a1c6 --- /dev/null +++ b/.agent/skills/frontend-rules/SKILL.md @@ -0,0 +1,544 @@ +--- +name: frontend-rules +description: >- + Frontend code quality reviewer for HTML, CSS, JS, images, fonts, network, + Core Web Vitals, design principles, accessibility, and animation. Audits code + against ruleskit standards and returns structured, actionable findings. +--- + +# SKILL: Frontend Agent + +# Generated by ruleskit — https://ruleskit.dev + +# Pack: Frontend | Framework: Framework-agnostic + +# Source: roadmap.sh/frontend-performance-best-practices + +## Role + +You are a Frontend code quality reviewer. +When a developer shares code, audit it against the rules below +and return structured, actionable findings only. + +## Rules + +## HTML Performance Rules + +- Deliver critical above-the-fold HTML first; use SSR or SSG to reduce TTFB +- Place all in — never below the fold +- Place +``` + +- **Never** use raw `fetch()` in ` +``` + +### Middleware (Client-Side Route Guards) + +```typescript +// middleware/auth.ts — named middleware +export default defineNuxtRouteMiddleware((to, from) => { + const user = useSupabaseUser(); // or your auth composable + if (!user.value && to.path !== "/login") { + return navigateTo("/login"); + } +}); + +// middleware/auth.global.ts — runs on EVERY navigation (expensive — use sparingly) +``` + +- Named middleware → applied via `definePageMeta({ middleware: ["auth"] })` +- Global middleware (`.global.ts`) → applied everywhere; keep logic minimal + +### Performance + +```typescript +// Payload extraction — avoid re-fetching on client hydration +// Nuxt serializes useAsyncData/useFetch results into the HTML payload automatically +// Ensure you're NOT disabling this with { server: false } unintentionally + +// useHead for per-page preload hints +useHead({ + link: [{ rel: "preload", as: "font", href: "/fonts/inter.woff2", crossorigin: "" }], +}); + +// Enable Brotli in nuxt.config.ts +export default defineNuxtConfig({ + nitro: { compressPublicAssets: { brotli: true } }, + routeRules: { + "/": { prerender: true }, // static + "/products/**": { isr: 3600 }, // ISR — revalidate every hour + "/dashboard/**": { ssr: true }, // always SSR (auth-gated) + }, +}); +``` diff --git a/packs/frontend/blocks/frameworks/sveltekit.md b/packs/frontend/blocks/frameworks/sveltekit.md index a8366c5..293d08b 100644 --- a/packs/frontend/blocks/frameworks/sveltekit.md +++ b/packs/frontend/blocks/frameworks/sveltekit.md @@ -1,9 +1,207 @@ ## SvelteKit Specific Rules -- Use load() in +page.server.js for SSR data — avoid client-side fetch waterfalls -- Use preload hints via inside -- Prefer +page.server.js over +page.js when data doesn't need to be reactive -- Use import() for heavy client-only components with {#await} -- Leverage built-in prerendering for static routes (prerender = true) -- Avoid hydration mismatches — keep server and client data consistent -- Use $app/environment browser check before accessing window or document +### Project & Folder Structure + +``` +src/ +├── routes/ +│ ├── +layout.svelte ← root layout; wraps all pages +│ ├── +layout.server.ts ← load() runs on server; data flows into layout +│ ├── +page.svelte ← / route +│ ├── +page.server.ts ← server-only load; never in client bundle +│ ├── products/ +│ │ ├── +page.svelte +│ │ ├── +page.server.ts +│ │ └── [id]/ +│ │ ├── +page.svelte +│ │ └── +page.server.ts +│ └── api/ +│ └── webhooks/ +│ └── +server.ts ← endpoint: POST /api/webhooks +├── lib/ +│ ├── server/ +│ │ └── db.ts ← server-only (never importable on client) +│ ├── components/ +│ │ └── ProductCard.svelte +│ └── index.ts ← re-exports from $lib +└── app.html ← root HTML template +``` + +- `+page.server.ts` — server-only load; returned data serialized via `JSON.stringify` +- `+page.ts` — runs on both server and client (good for client-navigations) +- `src/lib/server/` — SvelteKit enforces this cannot be imported in client code +- `$lib` alias → `src/lib/` — use this everywhere instead of relative `../../lib` + +### Load Functions — Server vs Universal + +```typescript +// +page.server.ts — runs ONLY on the server (DB access, auth, secrets) +import type { PageServerLoad } from "./$types"; +import { db } from "$lib/server/db"; +import { error } from "@sveltejs/kit"; + +export const load: PageServerLoad = async ({ params, locals }) => { + const product = await db.product.findUnique({ where: { id: params.id } }); + if (!product) error(404, "Product not found"); + + return { product }; // serialized to the page; Svelte types this for you +}; + +// +page.ts — runs on server (initial load) AND client (navigation) +// Use for: public API calls that don't need secrets +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ fetch, params }) => { + // Use the provided fetch — it's enhanced with SvelteKit's fetch interceptor + const res = await fetch(`/api/products/${params.id}`); + return { product: await res.json() }; +}; +``` + +**Rule**: Prefer `+page.server.ts` for anything touching a database, session, +or secret. Use `+page.ts` only for public API calls that must re-run on +client-side navigation. + +### Form Actions — Server Mutations + +```typescript +// +page.server.ts +import type { Actions } from "./$types"; +import { fail, redirect } from "@sveltejs/kit"; +import { z } from "zod"; + +export const actions: Actions = { + default: async ({ request, locals }) => { + const data = Object.fromEntries(await request.formData()); + const result = schema.safeParse(data); + if (!result.success) return fail(422, { errors: result.error.flatten() }); + + await db.product.create({ data: result.data }); + redirect(303, "/products"); + }, +}; +``` + +```svelte + + + +
+ + {#if form?.errors?.name} +

{form.errors.name}

+ {/if} + +
+``` + +- `use:enhance` — progressively enhances forms: no full page reload, but + gracefully degrades to native form if JS fails +- Always `fail(422, data)` for validation errors — never throw +- Always `redirect(303, path)` after a successful mutation (Post/Redirect/Get) + +### Reactive State — Svelte 5 Runes + +```svelte + +``` + +- Do not mix Svelte 4 syntax (`$:`, reactive `let`) with runes in the same file +- `$state` is deeply reactive — mutate arrays/objects directly (`items.push(x)`) +- Use `$state.snapshot(value)` to get a non-reactive plain copy for serialization + +### Server Endpoints + +```typescript +// src/routes/api/products/+server.ts +import type { RequestHandler } from "./$types"; +import { json, error } from "@sveltejs/kit"; + +export const GET: RequestHandler = async ({ url, locals }) => { + const limit = Number(url.searchParams.get("limit")) || 20; + const products = await db.product.findMany({ take: limit }); + return json(products); +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + const body = await request.json(); + // validate with zod + const product = await db.product.create({ data: body }); + return json(product, { status: 201 }); +}; +``` + +### Hooks & Middleware + +```typescript +// src/hooks.server.ts — runs on every server request +import type { Handle } from "@sveltejs/kit"; + +export const handle: Handle = async ({ event, resolve }) => { + // Auth: populate event.locals before load functions run + const session = await getSession(event.cookies.get("session")); + event.locals.user = session?.user ?? null; + + return resolve(event, { + // Inject HTML attributes into tag + transformPageChunk: ({ html }) => html.replace("%lang%", "en"), + }); +}; +``` + +### Route Protection Pattern + +```typescript +// src/routes/(dashboard)/+layout.server.ts +import { redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ locals }) => { + if (!locals.user) redirect(303, "/login"); + return { user: locals.user }; +}; +``` + +### Performance + +```typescript +// svelte.config.js — prerender and SSR options +import adapter from "@sveltejs/adapter-auto"; + +export default { + kit: { + adapter: adapter(), + prerender: { + handleMissingId: "warn", + }, + }, +}; + +// Per-route rendering options in +page.ts or +page.server.ts +export const prerender = true; // static generation +export const ssr = false; // CSR only (use sparingly — auth dashboards) +export const csr = false; // no client JS (pure HTML — forms still work) +``` + +- `browser` check: `import { browser } from "$app/environment"` before accessing `window` +- Never `await import("module")` at the top level in a universal load — it will + block the server render; use dynamic imports inside functions instead diff --git a/packs/frontend/blocks/frameworks/vanilla.md b/packs/frontend/blocks/frameworks/vanilla.md new file mode 100644 index 0000000..33c5c87 --- /dev/null +++ b/packs/frontend/blocks/frameworks/vanilla.md @@ -0,0 +1,227 @@ +## Vanilla JS / Framework-Agnostic Rules + +These rules apply when working without a framework — plain HTML, CSS, and +JavaScript, or when building a framework-agnostic library/component. + +### Project & Folder Structure + +``` +src/ +├── index.html +├── main.js ← entry point; minimal — just wires things together +├── components/ +│ ├── modal.js ← one class/function per file +│ └── carousel.js +├── lib/ +│ ├── dom.js ← DOM helpers ($, $$, createElement) +│ ├── events.js ← event bus or pub/sub +│ └── fetch.js ← typed fetch wrapper +├── state/ +│ └── store.js ← lightweight state container +└── styles/ + ├── base.css + ├── components/ + └── utilities.css +``` + +### DOM Query Helpers — Write Once, Use Everywhere + +```javascript +// lib/dom.js +const $ = (selector, root = document) => root.querySelector(selector); +const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; + +function createElement(tag, attrs = {}, ...children) { + const el = document.createElement(tag); + Object.entries(attrs).forEach(([k, v]) => { + if (k === "class") el.className = v; + else if (k.startsWith("on")) el.addEventListener(k.slice(2).toLowerCase(), v); + else el.setAttribute(k, v); + }); + children.flat().forEach(c => + el.appendChild(typeof c === "string" ? document.createTextNode(c) : c) + ); + return el; +} + +export { $, $$, createElement }; +``` + +### Component Pattern — Class with Lifecycle + +```javascript +// components/modal.js +export class Modal { + #root; + #onClose; + + constructor(selector, { onClose } = {}) { + this.#root = document.querySelector(selector); + this.#onClose = onClose ?? (() => {}); + this.#bind(); + } + + open() { + this.#root.removeAttribute("hidden"); + this.#root.setAttribute("aria-hidden", "false"); + document.body.style.overflow = "hidden"; + this.#root.querySelector("[data-autofocus]")?.focus(); + } + + close() { + this.#root.setAttribute("hidden", ""); + this.#root.setAttribute("aria-hidden", "true"); + document.body.style.overflow = ""; + this.#onClose(); + } + + #bind() { + this.#root.addEventListener("click", e => { + if (e.target === this.#root) this.close(); // backdrop click + }); + document.addEventListener("keydown", e => { + if (e.key === "Escape" && !this.#root.hasAttribute("hidden")) this.close(); + }); + } + + destroy() { + // remove event listeners before removing from DOM + this.#root.replaceWith(this.#root.cloneNode(true)); + } +} +``` + +### State Management — Lightweight Store + +```javascript +// state/store.js +export function createStore(initialState) { + let state = { ...initialState }; + const listeners = new Set(); + + return { + getState: () => ({ ...state }), + setState(updater) { + const next = typeof updater === "function" ? updater(state) : updater; + state = { ...state, ...next }; + listeners.forEach(fn => fn(state)); + }, + subscribe(fn) { + listeners.add(fn); + return () => listeners.delete(fn); // return unsubscribe + }, + }; +} + +// Usage +const store = createStore({ cart: [], user: null }); +const unsubscribe = store.subscribe(state => renderCart(state.cart)); +store.setState(s => ({ cart: [...s.cart, newItem] })); +``` + +### Event Delegation — Never Attach to Individual List Items + +```javascript +// BAD — attaches N listeners for N items +document.querySelectorAll(".product-card").forEach(card => { + card.addEventListener("click", handleClick); +}); + +// GOOD — one listener on the parent +document.querySelector(".product-list").addEventListener("click", e => { + const card = e.target.closest(".product-card"); + if (!card) return; + handleClick(card.dataset.id); +}); +``` + +### Fetch Wrapper — Typed, Error-Handled + +```javascript +// lib/fetch.js +export async function apiFetch(path, options = {}) { + const res = await fetch(`/api${path}`, { + ...options, + headers: { "Content-Type": "application/json", ...options.headers }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({ message: res.statusText })); + throw Object.assign(new Error(error.message), { status: res.status, data: error }); + } + + return res.status === 204 ? null : res.json(); +} +``` + +### Async Patterns + +```javascript +// Load data without blocking render — Intersection Observer for lazy load +const observer = new IntersectionObserver( + entries => entries.forEach(entry => { + if (!entry.isIntersecting) return; + loadContent(entry.target); + observer.unobserve(entry.target); + }), + { rootMargin: "200px" } +); + +document.querySelectorAll("[data-lazy]").forEach(el => observer.observe(el)); + +// Web Worker for CPU-heavy tasks +const worker = new Worker(new URL("./workers/sort.js", import.meta.url), { type: "module" }); +worker.postMessage({ items: largeArray }); +worker.addEventListener("message", e => renderSorted(e.data)); +``` + +### Custom Element Pattern (Web Components) + +```javascript +class ProductCard extends HTMLElement { + static observedAttributes = ["product-id", "name"]; + + connectedCallback() { + this.render(); + } + + attributeChangedCallback() { + this.render(); + } + + render() { + this.innerHTML = ` +
+

${this.getAttribute("name")}

+ +
+ `; + } +} + +customElements.define("product-card", ProductCard); +// Usage: +``` + +### Performance Rules Specific to Vanilla JS + +- Use `requestAnimationFrame` for all visual updates — never `setTimeout` for animation +- Use `requestIdleCallback` for non-urgent background work (analytics, prefetch) +- Batch DOM reads before DOM writes — never interleave (causes forced reflow): + +```javascript +// BAD — alternates read/write → layout thrash +boxes.forEach(box => { + const h = box.offsetHeight; // read → force layout + box.style.height = `${h * 2}px`; // write +}); + +// GOOD — batch reads, then batch writes +const heights = boxes.map(box => box.offsetHeight); // all reads +boxes.forEach((box, i) => (box.style.height = `${heights[i] * 2}px`)); // all writes +``` + +- Use `AbortController` to cancel fetch requests when the user navigates away +- Use `MutationObserver` instead of polling to react to DOM changes +- Remove all event listeners and observers in cleanup logic (SPA unmount, custom element `disconnectedCallback`) diff --git a/packs/frontend/pack.config.ts b/packs/frontend/pack.config.ts index a463fd3..fb10c74 100644 --- a/packs/frontend/pack.config.ts +++ b/packs/frontend/pack.config.ts @@ -16,17 +16,163 @@ const config: PackConfig = { { id: "vanilla", label: "Vanilla JS", frameworkFile: null }, ], blocks: [ - "html", - "css", - "javascript", - "comments", - "healing", - "images", - "fonts", - "network", - "web-vitals", + // ------------------------------------------------------------------ + // Always-on base blocks (domain: frontend/performance) + // ------------------------------------------------------------------ + { + id: "frontend/performance/html", + label: "HTML Performance", + file: "blocks/html.md", + packId: "frontend", + tags: ["performance", "html", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/css", + label: "CSS Performance", + file: "blocks/css.md", + packId: "frontend", + tags: ["performance", "css", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/javascript", + label: "JavaScript Performance", + file: "blocks/javascript.md", + packId: "frontend", + tags: ["performance", "javascript", "browser"], + inclusion: "always", + }, + { + id: "frontend/quality/comments", + label: "Code Comment Rules", + file: "blocks/comments.md", + packId: "frontend", + tags: ["quality", "comments"], + inclusion: "always", + }, + { + id: "frontend/healing/base", + label: "Self-Healing System", + file: "blocks/healing.md", + packId: "frontend", + tags: ["healing", "dx"], + inclusion: "always", + }, + { + id: "frontend/performance/images", + label: "Image Performance", + file: "blocks/images.md", + packId: "frontend", + tags: ["performance", "images", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/fonts", + label: "Font Performance", + file: "blocks/fonts.md", + packId: "frontend", + tags: ["performance", "fonts", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/network", + label: "Network & Caching", + file: "blocks/network.md", + packId: "frontend", + tags: ["performance", "network", "caching"], + inclusion: "always", + }, + { + id: "frontend/performance/web-vitals", + label: "Core Web Vitals", + file: "blocks/web-vitals.md", + packId: "frontend", + tags: ["performance", "vitals", "browser"], + inclusion: "always", + }, + + // ------------------------------------------------------------------ + // Optional blocks + // ------------------------------------------------------------------ + { + id: "frontend/design/base", + label: "Design Principles", + file: "blocks/design.md", + packId: "frontend", + tags: ["design", "a11y", "ux"], + inclusion: "optional", + }, + { + id: "frontend/quality/anti-slop", + label: "Anti-Slop Frontend Rules", + file: "blocks/anti-slop.md", + packId: "frontend", + tags: ["quality", "design", "anti-slop", "aesthetics"], + inclusion: "optional", + }, + { + // Child of anti-slop — resolver guarantees base block appears first. + // If a user selects tooling without the base, the resolver pulls the + // base in automatically via the parentId ancestry expansion. + id: "frontend/quality/anti-slop-tooling", + label: "Anti-Slop Tooling: Library Selection Guide", + file: "blocks/anti-slop-tooling.md", + packId: "frontend", + parentId: "frontend/quality/anti-slop", + tags: ["quality", "tooling", "anti-slop", "libraries"], + inclusion: "optional", + }, + + // ------------------------------------------------------------------ + // Framework-specific blocks (children of javascript performance base) + // ------------------------------------------------------------------ + { + id: "frontend/framework/nextjs", + label: "Next.js Specific Rules", + file: "blocks/frameworks/nextjs.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "nextjs", "react"], + inclusion: "framework", + }, + { + id: "frontend/framework/nuxt", + label: "Nuxt Specific Rules", + file: "blocks/frameworks/nuxt.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "nuxt", "vue"], + inclusion: "framework", + }, + { + id: "frontend/framework/sveltekit", + label: "SvelteKit Specific Rules", + file: "blocks/frameworks/sveltekit.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "sveltekit", "svelte"], + inclusion: "framework", + }, + { + id: "frontend/framework/angular", + label: "Angular Specific Rules", + file: "blocks/frameworks/angular.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "angular"], + inclusion: "framework", + }, + { + id: "frontend/framework/vanilla", + label: "Vanilla JS Specific Rules", + file: "blocks/frameworks/vanilla.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "vanilla", "no-framework"], + inclusion: "framework", + }, ], - optionalBlocks: [{ id: "design", label: "Design principles", default: true }], extras: [ { id: "husky", label: "Husky + lint-staged", default: true }, { id: "eslint", label: "ESLint", default: true }, diff --git a/packs/fullstack/pack.config.ts b/packs/fullstack/pack.config.ts index 4326831..a506cdf 100644 --- a/packs/fullstack/pack.config.ts +++ b/packs/fullstack/pack.config.ts @@ -1,32 +1,288 @@ import type { PackConfig } from "@/core/types"; +/** + * Fullstack pack — the complete union of frontend + backend blocks. + * + * Every block is referenced by its canonical id from the owning pack. + * The resolver deduplicates automatically: if a user also selects the + * standalone frontend or backend pack, no content appears twice. + */ const config: PackConfig = { id: "fullstack", label: "Full-Stack", status: "stable", - description: "End-to-end rules bridging UI performance, API design, security, database hygiene, and AI self-healing", + description: + "End-to-end rules bridging UI performance, API design, security, database hygiene, and AI self-healing", icon: "layers", source: "roadmap.sh/full-stack", frameworks: [ - { id: "agnostic", label: "Framework-agnostic", frameworkFile: null }, - { id: "nextjs", label: "Next.js (React)", frameworkFile: "nextjs.md" }, - { id: "nuxt", label: "Nuxt (Vue)", frameworkFile: "nuxt.md" }, - { id: "sveltekit", label: "SvelteKit", frameworkFile: "sveltekit.md" }, - { id: "laravel", label: "Laravel (PHP)", frameworkFile: "laravel.md" }, + { id: "agnostic", label: "Framework-agnostic", frameworkFile: null }, + { id: "nextjs", label: "Next.js (React)", frameworkFile: "nextjs.md" }, + { id: "nuxt", label: "Nuxt (Vue)", frameworkFile: "nuxt.md" }, + { id: "sveltekit", label: "SvelteKit", frameworkFile: "sveltekit.md" }, + { id: "angular", label: "Angular", frameworkFile: null }, + { id: "vanilla", label: "Vanilla JS", frameworkFile: null }, + { id: "laravel", label: "Laravel (PHP)", frameworkFile: "laravel.md" }, + { id: "express", label: "Express.js", frameworkFile: null }, + { id: "nestjs", label: "NestJS", frameworkFile: null }, + { id: "fastify", label: "Fastify", frameworkFile: null }, + { id: "django", label: "Django", frameworkFile: null }, ], blocks: [ - "general", - "frontend", - "backend", - "database", - ], - optionalBlocks: [ - { id: "healing", label: "AI self-healing workflow", default: true }, + // ------------------------------------------------------------------ + // Fullstack-owned: general cross-cutting principles + // ------------------------------------------------------------------ + { + id: "fullstack/general/principles", + label: "General Principles", + file: "blocks/general.md", + packId: "fullstack", + tags: ["general", "principles", "quality"], + inclusion: "always", + }, + + // ------------------------------------------------------------------ + // FRONTEND — always-on (full set, same as frontend pack) + // ------------------------------------------------------------------ + { + id: "frontend/performance/html", + label: "HTML Performance", + file: "blocks/html.md", + packId: "frontend", + tags: ["performance", "html", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/css", + label: "CSS Performance", + file: "blocks/css.md", + packId: "frontend", + tags: ["performance", "css", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/javascript", + label: "JavaScript Performance", + file: "blocks/javascript.md", + packId: "frontend", + tags: ["performance", "javascript", "browser"], + inclusion: "always", + }, + { + id: "frontend/quality/comments", + label: "Code Comment Rules", + file: "blocks/comments.md", + packId: "frontend", + tags: ["quality", "comments"], + inclusion: "always", + }, + { + id: "frontend/healing/base", + label: "Self-Healing System", + file: "blocks/healing.md", + packId: "frontend", + tags: ["healing", "dx"], + inclusion: "always", + }, + { + id: "frontend/performance/images", + label: "Image Performance", + file: "blocks/images.md", + packId: "frontend", + tags: ["performance", "images", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/fonts", + label: "Font Performance", + file: "blocks/fonts.md", + packId: "frontend", + tags: ["performance", "fonts", "browser"], + inclusion: "always", + }, + { + id: "frontend/performance/network", + label: "Network & Caching", + file: "blocks/network.md", + packId: "frontend", + tags: ["performance", "network", "caching"], + inclusion: "always", + }, + { + id: "frontend/performance/web-vitals", + label: "Core Web Vitals", + file: "blocks/web-vitals.md", + packId: "frontend", + tags: ["performance", "vitals", "browser"], + inclusion: "always", + }, + + // ------------------------------------------------------------------ + // BACKEND — always-on (full set, same as backend pack) + // ------------------------------------------------------------------ + { + id: "backend/api/design", + label: "API Design", + file: "blocks/api-design.md", + packId: "backend", + tags: ["api", "design", "rest"], + inclusion: "always", + }, + { + id: "backend/security/base", + label: "Security", + file: "blocks/security.md", + packId: "backend", + tags: ["security", "auth"], + inclusion: "always", + }, + { + id: "backend/data/database", + label: "Database", + file: "blocks/database.md", + packId: "backend", + tags: ["database", "sql"], + inclusion: "always", + }, + { + id: "backend/reliability/error-handling", + label: "Error Handling", + file: "blocks/error-handling.md", + packId: "backend", + tags: ["error-handling", "reliability"], + inclusion: "always", + }, + { + id: "backend/performance/base", + label: "Backend Performance", + file: "blocks/performance.md", + packId: "backend", + tags: ["performance", "server"], + inclusion: "always", + }, + + // ------------------------------------------------------------------ + // OPTIONAL — full set from both frontend and backend + // ------------------------------------------------------------------ + { + id: "frontend/design/base", + label: "Design Principles", + file: "blocks/design.md", + packId: "frontend", + tags: ["design", "a11y", "ux"], + inclusion: "optional", + }, + { + id: "frontend/quality/anti-slop", + label: "Anti-Slop Frontend Rules", + file: "blocks/anti-slop.md", + packId: "frontend", + tags: ["quality", "design", "anti-slop", "aesthetics"], + inclusion: "optional", + }, + { + id: "frontend/quality/anti-slop-tooling", + label: "Anti-Slop Tooling: Library Selection Guide", + file: "blocks/anti-slop-tooling.md", + packId: "frontend", + parentId: "frontend/quality/anti-slop", + tags: ["quality", "tooling", "anti-slop", "libraries"], + inclusion: "optional", + }, + { + id: "backend/data/caching", + label: "Caching Strategies", + file: "blocks/caching.md", + packId: "backend", + tags: ["caching", "performance", "server"], + inclusion: "optional", + }, + { + id: "backend/architecture/base", + label: "Code Architecture", + file: "blocks/architecture.md", + packId: "backend", + tags: ["architecture", "patterns"], + inclusion: "optional", + }, + + // ------------------------------------------------------------------ + // FRAMEWORK — frontend frameworks (point at canonical frontend files) + // ------------------------------------------------------------------ + { + id: "frontend/framework/nextjs", + label: "Next.js Rules", + file: "blocks/frameworks/nextjs.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "nextjs", "react"], + inclusion: "framework", + }, + { + id: "frontend/framework/nuxt", + label: "Nuxt Rules", + file: "blocks/frameworks/nuxt.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "nuxt", "vue"], + inclusion: "framework", + }, + { + id: "frontend/framework/sveltekit", + label: "SvelteKit Rules", + file: "blocks/frameworks/sveltekit.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "sveltekit", "svelte"], + inclusion: "framework", + }, + { + id: "frontend/framework/angular", + label: "Angular Rules", + file: "blocks/frameworks/angular.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "angular"], + inclusion: "framework", + }, + { + id: "frontend/framework/vanilla", + label: "Vanilla JS Rules", + file: "blocks/frameworks/vanilla.md", + packId: "frontend", + parentId: "frontend/performance/javascript", + tags: ["framework", "vanilla", "no-framework"], + inclusion: "framework", + }, + + // ------------------------------------------------------------------ + // FRAMEWORK — backend frameworks (point at canonical backend files) + // ------------------------------------------------------------------ + { + id: "backend/framework/laravel", + label: "Laravel Rules", + file: "blocks/frameworks/laravel.md", + packId: "backend", + parentId: "backend/security/base", + tags: ["framework", "laravel", "php"], + inclusion: "framework", + }, + { + id: "backend/framework/express", + label: "Express.js Rules", + file: "blocks/frameworks/express.md", + packId: "backend", + parentId: "backend/security/base", + tags: ["framework", "express", "nodejs"], + inclusion: "framework", + }, ], extras: [ - { id: "husky", label: "Husky pre-commit hook", default: true }, - { id: "linter", label: "Linter (ESLint/Flake8/etc.)", default: true }, - { id: "prettier", label: "Prettier formatter", default: true }, + { id: "husky", label: "Husky pre-commit hook", default: true }, + { id: "linter", label: "Linter (ESLint / Flake8 / etc.)", default: true }, + { id: "prettier", label: "Prettier formatter", default: true }, + { id: "backend-formatter", label: "Backend formatter (config file)", default: false }, + { id: "ai-prompt", label: "AI generation prompt", default: false }, ], }; diff --git a/src/components/Generator.tsx b/src/components/Generator.tsx index 84450d6..22c830d 100644 --- a/src/components/Generator.tsx +++ b/src/components/Generator.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, useEffect } from "react"; import { registry } from "@/core/registry"; import { generate, buildCliCommand } from "@/core/generator"; -import type { OutputFormat, GeneratedFile, PackConfig } from "@/core/types"; +import type { OutputFormat, GeneratedFile, PackConfig, RuleBlock } from "@/core/types"; const FORMAT_OPTIONS: { id: OutputFormat; label: string; ext: string }[] = [ { id: "cursorrules", label: "Cursor", ext: ".cursorrules" }, @@ -10,6 +10,14 @@ const FORMAT_OPTIONS: { id: OutputFormat; label: string; ext: string }[] = [ { id: "claude", label: "Claude Code", ext: "CLAUDE.md" }, ]; +/** + * Derives the list of togglable optional rule blocks from the new RuleBlock[] + * structure. Previously this read from the deprecated `pack.optionalBlocks` field. + */ +function getOptionalRuleBlocks(pack: PackConfig): RuleBlock[] { + return pack.blocks.filter((b) => b.inclusion === "optional"); +} + interface Props { pack: PackConfig; } @@ -18,7 +26,9 @@ export function Generator({ pack }: Props) { const [formats, setFormats] = useState(["cursorrules"]); const [frameworkId, setFrameworkId] = useState(pack.frameworks[0]?.id ?? "agnostic"); const [optionalBlocks, setOptionalBlocks] = useState( - (pack.optionalBlocks ?? []).filter((b) => b.default).map((b) => b.id), + // Read from the new RuleBlock[] structure — blocks with inclusion: "optional" and default: true + // (Note: RuleBlock has no "default" field; we default-enable none and let the user choose) + [], ); const [extras, setExtras] = useState( pack.extras.filter((e) => e.default).map((e) => e.id), @@ -26,6 +36,8 @@ export function Generator({ pack }: Props) { const [activeTab, setActiveTab] = useState(0); const [copied, setCopied] = useState(null); + const optionalRuleBlocks = getOptionalRuleBlocks(pack); + const files: GeneratedFile[] = useMemo( () => generate({ @@ -40,7 +52,7 @@ export function Generator({ pack }: Props) { useEffect(() => { setFrameworkId(pack.frameworks[0]?.id ?? "agnostic"); - setOptionalBlocks((pack.optionalBlocks ?? []).filter((b) => b.default).map((b) => b.id)); + setOptionalBlocks([]); // reset optional blocks on pack change setExtras(pack.extras.filter((e) => e.default).map((e) => e.id)); setActiveTab(0); setCopied(null); @@ -134,7 +146,7 @@ export function Generator({ pack }: Props) { {/* Step 3: Extras */}
- {(pack.optionalBlocks ?? []).map((b) => ( + {optionalRuleBlocks.map((b) => (