Skip to content
Back to skills

Promote Component

ASecurity

Use to refactor a React/RN component up the colocation hierarchy (L0 → L1 → L2) when its usage spreads across pages. Two modes: "scan candidates" counts every component''s usages across the codebase and reports the promotion candidates against the Rule of Three; "promote <Component>" moves the file to the right level, updates all imports. Reads `.workflow/meta.json#stack.framework` to honor next / expo-rn / monorepo conventions. Triggers on: "promote PostCard", "scan promotion candidates", "t...

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
developmenttypescriptpythongobashreactnextjsrefactoringgit

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add lukedj78/dev-flow --skill promote-component --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Promote Component?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Promote Component
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lukedj78-promote-component/badge)](https://www.skillsdirectory.com/skills/lukedj78-promote-component)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: promote-component
description: 'Use to refactor a React/RN component up the colocation hierarchy (L0 → L1 → L2) when its usage spreads across pages. Two modes: "scan candidates" counts every component''s usages across the codebase and reports the promotion candidates against the Rule of Three; "promote <Component>" moves the file to the right level, updates all imports. Reads `.workflow/meta.json#stack.framework` to honor next / expo-rn / monorepo conventions. Triggers on: "promote PostCard", "scan promotion candidates", "this component is used everywhere, lift it up", "questo componente è usato in più pagine, promuovilo", "refactor components/shared/". Not for: creating new components (use design-md-to-app or screenshot-to-page or rn-add-screen), splitting compound components into a folder, or moving files across different concerns — this is purely vertical promotion.'
---

# promote-component — Rule of Three automation

## Contract

See `references/contracts.md` (vendored from `dev-flow`). Key facts:
- Reads `<project-root>/.workflow/meta.json#stack.framework` (must be `"next"`, `"expo-rn"`, or `"monorepo"`).
- Reads `meta.json#stack.route_groups` (optional, helps detect the target level).
- Modifies code in `<project-root>/` (or `<project-root>/apps/<web|mobile>/` for monorepo).
- Does NOT modify `meta.json#phase`. Appends to `meta.json#history`.
- Always idempotent: re-running the scan reports the same candidates; re-running a promote on an already-promoted component does nothing.

## When this skill applies

- User says "scan promotion candidates" / "scansiona candidati promozione" → run the scan.
- User says "promote <Component>" / "promovi <Component>" → execute the move.
- The user is reviewing the codebase after a feature lands and wants to clean up duplication.

Orchestrator does NOT route here automatically — invoked on demand.

## Knowledge dependencies

- `composition-patterns-guide/SKILL.md` — the 7 Vercel composition rules + our colocation rules.
- `references/colocation-rules.md` — the canonical Rule of Three + L0/L1/L2 spec.

## Web vs mobile targets (read this first)

**Next.js App Router** supports `_`-prefixed private folders — `app/<route>/_components/` is a valid non-routable convention there. **Expo Router does not**: every file under `app/` (aside from a short reserved list) becomes a real route, so `app/<route>/_components/` would create a ghost route on mobile, not a private folder. **Settled upstream, not pending**: the request ([expo/expo#44696](https://github.com/expo/expo/issues/44696)) was closed **`won't fix`** on 2026-06-01 — Expo Router wants components outside `app/` by design. Confirmed mechanically at `expo-router@58.0.23` (2026-10-17): no underscore rule in the route scanner. Treat mobile `app/` as routes-only, permanently. See `references/colocation-rules.md`.

Consequently this skill uses **different scan/target paths per platform** — see `references/colocation-rules.md` for the full model and rationale. Summary:

| | Web (`stack.framework == "next"`, or `apps/web` in a monorepo) | Mobile (`stack.framework == "expo-rn"`, or `apps/mobile` in a monorepo) |
|---|---|---|
| Candidate scan root | `app/**/_components/*.tsx` **and** `components/<area>/**/*.tsx` (excludes `components/{ui,theme,shared,brand,logos}`) | `components/<feature>/*.tsx` (excludes `components/{ui,theme,shared}`) |
| L0 target | `app/<route>/_components/<Component>.tsx` | `components/<feature>/<Component>.tsx` |
| L1 target | `app/(group)/_components/<Component>.tsx` | same folder as L0 (no separate L1 path on mobile — see colocation-rules.md) |
| L2 target | `components/shared/<dominio>/<Component>.tsx` | `components/shared/<dominio>/<Component>.tsx` (identical) |

Everything below is written generically ("the platform's `_components/`-or-`components/<feature>/` root") — substitute per the table above. The scripts (`scripts/scan_promotion.py`, `scripts/promote.py`) branch on `meta.json#stack.framework` automatically.

## Workflow — `scan` mode

### Step 1 — Detect framework + cwd

Read `meta.json#stack.framework`:
- `"next"` → cwd = project root, scan `app/**/_components/` **and `components/<area>/`** (excluding `components/{ui,theme,shared,brand,logos}`). Many web projects keep feature components in `components/<area>/` and have no `_components/` at all — smart-travel has 200+ there, and a scan of `_components/` alone reported nothing (2026-10-12).
- `"expo-rn"` → cwd = project root, scan `components/<feature>/` (excluding `components/{ui,theme,shared}`).
- `"monorepo"` → scan `apps/web/` with the web rules and `apps/mobile/` with the mobile rules, separately. Print two tables.

### Step 2 — Find every promotion candidate

**Web**:
```bash
find app -type d -name "_components" -exec ls -1 {} \;
```
For each `.tsx` file in `_components/`, record component name, source path (e.g., `app/(app)/posts/_components/PostCard.tsx`), and current level (L0: under a leaf route, L1: under `app/(group)/_components/`).

Then, still on web, every `.tsx` under `components/<area>/` (not `ui`, `theme`, `shared`, `brand`, `logos`). Count its importers **by resolved path** (`@/components/<area>/x` or a relative `./x`), not by file name — two `parts.tsx` in different areas are different components. The pages of one route group are an area's natural consumers; it becomes an **L2 candidate** (`components/shared/<dominio>/`) when, with 3+ importers, another `components/<area>` imports it or two or more route groups do. `scripts/scan_promotion.py` prints this as a second table.

**Mobile**:
```bash
find components -mindepth 2 -maxdepth 2 -name "*.tsx" | grep -vE '/(ui|theme|shared)/'
```
For each match, record component name, source path (e.g., `components/posts/PostCard.tsx`), and which feature folder(s) hold a copy of that name (duplicates across feature folders are the mobile promotion signal — see Step 4).

### Step 3 — Count usages

For every component, grep the codebase for imports referencing its file path or filename (both platforms):

```bash
grep -rln "from .*['\"].*PostCard['\"]" app/ components/ --include="*.tsx" --include="*.ts"
```

Count distinct import sites (deduplicate by file).

### Step 4 — Compute suggestions per Rule of Three

**Web**:

| Usage count | Current level | Suggestion |
|---|---|---|
| 1 | L0 | OK — stays |
| 2 | L0 | Tolerated duplicate — wait the 3rd |
| 3+, all in same route group | L0 (in 2+ pages) | Promote to L1 (`app/(group)/_components/`) |
| 3+, across different route groups | L0 or L1 | Promote to L2 (`components/shared/<dominio>/`) |
| 2+, but on its way up | L1 → L2 | Promote to L2 |

**Mobile** (mobile has no separate L1 path — see colocation-rules.md):

| Copies across feature folders | Suggestion |
|---|---|
| 1 | OK — stays |
| 2 | Tolerated duplicate — wait the 3rd |
| 3+, reconcilable to one feature/domain | Merge duplicates into one `components/<feature>/<Component>.tsx` |
| 3+, spanning different business domains | Promote to L2 (`components/shared/<dominio>/`) |

### Step 5 — Print the report

```
Promotion candidates in apps/web/ (next):

| Component                  | Usages | Current level | Suggestion                              |
|----------------------------|--------|---------------|------------------------------------------|
| PostCard                   | 3      | L0            | Promote to L1 — app/(app)/_components/   |
| UserAvatar                 | 4      | L0            | Promote to L2 — components/shared/user/  |
| BillingSummary             | 2      | L0            | Wait the 3rd use                         |
| PricingTable               | 5      | L0            | Promote to L2 — components/shared/billing/ |

Promotion candidates in apps/mobile/ (expo-rn):

| Component                  | Usages | Current level | Suggestion                                |
|----------------------------|--------|---------------|--------------------------------------------|
| PostCard                   | 3      | L0            | Promote to L2 — components/shared/post/    |
| SettingsRow                | 2      | L0            | Wait the 3rd use                           |
```

Ask the user: "Vuoi promuoverne alcuni? (Y/n, oppure dimmi quali)".

## Workflow — `promote <Component>` mode

### Step 1 — Locate the source file

**Web**: find the file matching `<Component>` under `app/**/_components/`.
**Mobile**: find the file matching `<Component>` under `components/<feature>/` (excluding `components/{ui,theme,shared}`).

If multiple matches (e.g., two copies at L0):
- The "canonical" copy is the older / most-complete one.
- Diff the two; flag any divergence to the user for resolution.

### Step 2 — Determine target level

If invoked without an explicit target, recompute from usage/copy distribution:
- **Web**: same route group, 3+ usages → L1: `app/(group)/_components/<Component>.tsx`. Different groups, 3+ usages → L2: `components/shared/<dominio>/<Component>.tsx`.
- **Mobile**: copies in 3+ feature folders reconcilable to one domain → merge into `components/<feature>/<Component>.tsx` (no move, dedupe only). Copies spanning different business domains → L2: `components/shared/<dominio>/<Component>.tsx`.

Ask the user for `<dominio>` if not obvious (either platform). If an explicit target is provided ("promote PostCard to L2 in components/shared/post/"), honor it.

### Step 3 — Move the file

**Web**:
```bash
# L0 → L1 (same group)
git mv app/(app)/posts/_components/PostCard.tsx app/(app)/_components/PostCard.tsx

# L0 → L2 (cross-group)
mkdir -p components/shared/post
git mv app/(app)/posts/_components/PostCard.tsx components/shared/post/PostCard.tsx
```

**Mobile** (never touches `app/` — components never live there):
```bash
# Dedupe (3rd use reconciled to one feature) — keep the canonical copy in place, remove the others
rm components/profile/PostCard.tsx   # duplicate removed, canonical stays at components/posts/PostCard.tsx

# L0 → L2 (cross-domain)
mkdir -p components/shared/post
git mv components/posts/PostCard.tsx components/shared/post/PostCard.tsx
```

If duplicates existed at L0 (the "2 copies" case), also `rm` them (both platforms).

### Step 4 — Update all imports

Scan all `.tsx` / `.ts` files in `app/` and `components/`, find imports referencing the OLD path or filename, rewrite to the NEW path. Applies identically on both platforms — the import-rewrite step never assumes a platform-specific path shape.

Pattern types:
- Web: `from "@/app/(app)/posts/_components/PostCard"` → `from "@/components/shared/post/PostCard"`
- Mobile: `from "@/components/posts/PostCard"` → `from "@/components/shared/post/PostCard"`
- `from "./PostCard"` (siblings) → resolve to absolute, then rewrite
- `from "../PostCard"` (parent) → idem

Use a TypeScript-aware import rewrite (e.g., `ts-morph` Python equivalent via jscodeshift or simple regex with care).

### Step 5 — Verify with tsc

```bash
npx tsc --noEmit
```

Must pass. If it fails:
1. Inspect the error: usually a path that wasn't rewritten.
2. Fix it manually.
3. Re-verify.

If still failing after 2 attempts, ROLLBACK the move (`git restore`) and report to the user.

### Step 6 — Commit + history

```bash
git add -A
git commit -m "refactor: promote <Component> from <old-level> to <new-level>"
```

Append to `meta.json#history`:
```json
{
  "skill": "promote-component",
  "ran_at": "<iso>",
  "inputs": {"component": "PostCard", "from": "L0 (3 sites)", "to": "L2", "domain": "post"},
  "outputs": {"new_path": "components/shared/post/PostCard.tsx", "imports_updated": 3}
}
```

## Common anti-patterns (NEVER do)

- ❌ Promote at the 2nd use — wait the 3rd. The premature abstraction is more expensive than the duplicate.
- ❌ Move without rewriting imports — broken project.
- ❌ Promote a compound component piece by piece — the whole compound moves together, always.
- ❌ Use generic domain names (`shared/common/`, `shared/misc/`) — use the business domain.
- ❌ Promote across route groups WITHOUT going to L2 — if 2+ groups use it, it MUST live in `components/shared/`.
- ❌ Skip the tsc verification — silent broken imports propagate.

## Updating meta.json (recommended pattern)

```bash
python3 .../dev-flow/scripts/update_meta.py <project-root> append-history \
    --skill 'promote-component' --inputs '{"component": "<X>"}' --outputs '{"new_path": "<path>"}'
```

## Sources

- Spec: `docs/superpowers/specs/2026-06-06-folder-structure-refactor.md`
- Sandi Metz, "The Wrong Abstraction" (2016): https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
- "Rule of Three" pattern: documented in "The Pragmatic Programmer" + multiple refactoring books.

Files in this skill

  • SKILL.md12.5 KB
  • references/colocation-rules.md11.4 KB
  • references/contracts.md80.5 KB
  • scripts/promote.py11.9 KB
  • scripts/scan_promotion.py13 KB
  • scripts/test_scan_promotion.py5.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.