navori 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +49 -11
- package/dist/assets/core/core-assets/lib-skills/axios.md +62 -0
- package/dist/assets/core/core-assets/lib-skills/mantine-form.md +58 -0
- package/dist/assets/core/core-assets/lib-skills/react-router.md +61 -0
- package/dist/assets/core/core-assets/presets/monorepo-turbopnpm/managed/stack.md +11 -0
- package/dist/assets/core/core-assets/presets/monorepo-turbopnpm/skills/turbo-workspaces.md +50 -0
- package/dist/assets/core/core-assets/presets/monorepo-turbopnpm.json +23 -0
- package/dist/index.js +1545 -803
- package/package.json +3 -3
|
@@ -24,22 +24,60 @@ extract_cmd() {
|
|
|
24
24
|
}
|
|
25
25
|
cmd=$(extract_cmd)
|
|
26
26
|
|
|
27
|
+
# Detect the project's REAL package manager from lockfiles / package.json, so a
|
|
28
|
+
# gate command hardcoded to one PM (e.g. `pnpm run ...`) can still run in a repo
|
|
29
|
+
# that actually uses another (e.g. bun). Mirrors lib/detect.ts precedence:
|
|
30
|
+
# `packageManager` field first, then pnpm/bun/yarn/npm lockfiles. Prints the PM
|
|
31
|
+
# name (or nothing) on stdout. Git runs hooks from the repo root, so relative
|
|
32
|
+
# paths resolve against the project root.
|
|
33
|
+
detect_pm() {
|
|
34
|
+
if [ -f package.json ]; then
|
|
35
|
+
pm=$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' package.json | head -n1)
|
|
36
|
+
case "$pm" in pnpm|npm|yarn|bun) printf '%s' "$pm"; return 0 ;; esac
|
|
37
|
+
fi
|
|
38
|
+
if [ -f pnpm-lock.yaml ]; then printf 'pnpm'; return 0; fi
|
|
39
|
+
if [ -f bun.lockb ] || [ -f bun.lock ]; then printf 'bun'; return 0; fi
|
|
40
|
+
if [ -f yarn.lock ]; then printf 'yarn'; return 0; fi
|
|
41
|
+
if [ -f package-lock.json ]; then printf 'npm'; return 0; fi
|
|
42
|
+
return 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# True when the argument is a JS package manager (so we only try to remap the
|
|
46
|
+
# leading token of the gate command when it's actually a PM, not `make`/`ruff`).
|
|
47
|
+
is_pm() {
|
|
48
|
+
case "$1" in pnpm|npm|yarn|bun) return 0 ;; *) return 1 ;; esac
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
run_gate() {
|
|
52
|
+
echo "[navori] running quality-gate fast: $1" >&2
|
|
53
|
+
eval "$1" || {
|
|
54
|
+
echo "[navori] quality-gate fast failed. Commit/push aborted." >&2
|
|
55
|
+
exit 2
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
case "$cmd" in
|
|
28
60
|
'git commit'*|'git push'*)
|
|
29
61
|
gate="{{qualityGate.fast}}"
|
|
30
|
-
# Defensive: skip cleanly (exit 0) when the gate's runtime isn't on PATH —
|
|
31
|
-
# e.g. a Nix/nvm shell that hasn't loaded the project env. Never block a
|
|
32
|
-
# commit just because the tool is missing from THIS shell.
|
|
33
62
|
gate_bin="${gate%% *}"
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
if command -v "$gate_bin" >/dev/null 2>&1; then
|
|
64
|
+
run_gate "$gate"
|
|
65
|
+
else
|
|
66
|
+
# The declared runner isn't on PATH. If it's a package manager, detect the
|
|
67
|
+
# repo's real one from lockfiles and retry through it (a `pnpm run x` gate
|
|
68
|
+
# still runs in a bun-only checkout). #88: NEVER skip the gate silently —
|
|
69
|
+
# when nothing can run it, BLOCK the commit loudly instead of the old
|
|
70
|
+
# `exit 0` that handed a contributor zero quality gate without a word.
|
|
71
|
+
detected_pm="$(detect_pm)"
|
|
72
|
+
if is_pm "$gate_bin" && [ -n "$detected_pm" ] && [ "$detected_pm" != "$gate_bin" ] && command -v "$detected_pm" >/dev/null 2>&1; then
|
|
73
|
+
echo "[navori] '$gate_bin' no está en PATH; uso el package manager detectado por lockfile: '$detected_pm'." >&2
|
|
74
|
+
run_gate "$detected_pm ${gate#* }"
|
|
75
|
+
else
|
|
76
|
+
echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
|
|
77
|
+
echo "[navori] Commit/push BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin' o usa 'git commit --no-verify' si de verdad quieres saltártelo." >&2
|
|
78
|
+
exit 2
|
|
79
|
+
fi
|
|
37
80
|
fi
|
|
38
|
-
echo "[navori] running quality-gate fast: $gate" >&2
|
|
39
|
-
eval "$gate" || {
|
|
40
|
-
echo "[navori] quality-gate fast failed. Commit/push aborted." >&2
|
|
41
|
-
exit 2
|
|
42
|
-
}
|
|
43
81
|
;;
|
|
44
82
|
esac
|
|
45
83
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axios
|
|
3
|
+
description: Patrones de Axios en TS — instancia central, interceptores, tipado de respuestas, manejo de errores y cancelación. Aplica al tocar llamadas HTTP a APIs.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Axios — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al hacer una llamada HTTP a una API: crear un endpoint del cliente, agregar auth, mapear errores, o cancelar una request. Axios se cablea **una vez** en una instancia central con interceptores — los componentes/servicios importan esa instancia, no `axios` crudo con la URL a mano en cada llamada.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
Una instancia por API (baseURL + interceptores), funciones tipadas encima; nunca `axios.get(fullUrl)` disperso:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export const api = axios.create({
|
|
19
|
+
baseURL: import.meta.env.VITE_APP_NEXUS_URL,
|
|
20
|
+
timeout: 15_000,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
api.interceptors.request.use((config) => {
|
|
24
|
+
const token = getToken();
|
|
25
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
26
|
+
return config;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
api.interceptors.response.use(
|
|
30
|
+
(res) => res,
|
|
31
|
+
(error) => Promise.reject(normalizeError(error)), // un solo shape de error
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Servicio tipado: el genérico es el TIPO DE DATA, no el del envelope.
|
|
35
|
+
export const getSession = (id: string) =>
|
|
36
|
+
api.get<Session>(`/sessions/${id}`).then((r) => r.data);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Gotchas que muerden
|
|
40
|
+
|
|
41
|
+
- **`api.get<T>()` tipa `response.data`, no la respuesta entera.** El `T` describe el `data`; Axios envuelve en `{ data, status, headers }`. Devuelve `r.data` desde el servicio para que el caller vea `Session`, no `AxiosResponse<Session>`.
|
|
42
|
+
- **Una instancia con `baseURL`, no la URL completa por llamada.** Centraliza host/timeout/headers. Repetir `axios.get('https://…/sessions')` esparce la config y rompe al cambiar de entorno.
|
|
43
|
+
- **Auth/refresh/logging en interceptores, no copiados en cada request.** El token va en un `request.use`; el 401→refresh y el mapeo de error van en `response.use`. Nada de `headers: { Authorization }` a mano en cada endpoint.
|
|
44
|
+
- **`axios.isAxiosError(err)` antes de leer `err.response`.** En el `catch`, `err` es `unknown`. Sin el type guard, `err.response.data` explota en errores de red (donde `response` es `undefined` y solo hay `err.request`).
|
|
45
|
+
- **Un error de red NO es un error HTTP.** Timeout/DNS/offline no traen `response`. Distingue `err.response` (el server respondió con 4xx/5xx) de `err.request` (nunca llegó) para dar el mensaje correcto.
|
|
46
|
+
- **Cancela requests en vuelo con `AbortController`.** En efectos/búsqueda-as-you-type pasa `{ signal: controller.signal }` y aborta en el cleanup; sin esto, una respuesta vieja pisa a una nueva (race).
|
|
47
|
+
- **4xx/5xx ya rechazan la promesa.** No revises `res.status` en el `.then`; el flujo de error vive en `catch`/el interceptor. Solo `validateStatus` cambia esa regla, y rara vez la necesitas.
|
|
48
|
+
|
|
49
|
+
## Reglas duras
|
|
50
|
+
|
|
51
|
+
1. Todo HTTP pasa por la instancia central con `baseURL` + interceptores; nada de `axios` crudo con URL completa suelta.
|
|
52
|
+
2. Auth, refresh y normalización de error en interceptores, una sola vez.
|
|
53
|
+
3. Servicios tipados con `api.get<Data>(...)` que devuelven `.data`; el genérico es la data, no el envelope.
|
|
54
|
+
4. En `catch`, `axios.isAxiosError` antes de tocar `.response`; distingue error de red de error HTTP.
|
|
55
|
+
5. Requests cancelables (`AbortController`) donde puede haber carreras.
|
|
56
|
+
|
|
57
|
+
## Antes de declarar listo
|
|
58
|
+
|
|
59
|
+
- La llamada usa la instancia central; sin URLs absolutas ni headers de auth repetidos.
|
|
60
|
+
- Respuestas tipadas devolviendo `.data`; el error se maneja con `isAxiosError` y un shape único.
|
|
61
|
+
- Requests que compiten se cancelan en el cleanup.
|
|
62
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mantine-form
|
|
3
|
+
description: Patrones de @mantine/form — useForm, getInputProps, validación con zodResolver, campos anidados y listas. Aplica al crear o tocar formularios con Mantine.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Mantine Form — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al crear o tocar un formulario con `@mantine/form`: cablear inputs de Mantine, validar, manejar submit, o campos anidados/listas. `useForm` es la fuente de verdad del form — no espejees sus valores en `useState`, y valida con un schema Zod (vía `mantine-form-zod-resolver`), no con funciones sueltas por campo.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
`useForm` + `getInputProps` (esparce value/onChange/error de un jalón) + `zodResolver` para el schema:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const schema = z.object({
|
|
19
|
+
email: z.string().email(),
|
|
20
|
+
role: z.enum(['coach', 'coachee']),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const form = useForm({
|
|
24
|
+
mode: 'uncontrolled', // menos re-renders; el default recomendado
|
|
25
|
+
initialValues: { email: '', role: 'coachee' },
|
|
26
|
+
validate: zodResolver(schema), // desde 'mantine-form-zod-resolver'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
<form onSubmit={form.onSubmit((values) => save(values))}>
|
|
30
|
+
<TextInput {...form.getInputProps('email')} />
|
|
31
|
+
<Select data={['coach', 'coachee']} {...form.getInputProps('role')} />
|
|
32
|
+
<Button type="submit">Guardar</Button>
|
|
33
|
+
</form>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Gotchas que muerden
|
|
37
|
+
|
|
38
|
+
- **`getInputProps('campo')` cablea todo; no lo desarmes.** Ya trae `value`/`onChange`/`error`/`onBlur`. Pasar `value`/`onChange` a mano encima rompe el binding — deja que el spread mande.
|
|
39
|
+
- **`mode: 'uncontrolled'` cambia cómo lees valores.** En uncontrolled, `form.values` no re-renderiza al teclear; para reflejar un campo en la UI usa `form.watch('campo')` o `form.getValues()`. En `controlled` sí re-renderiza cada tecla (más caro en forms grandes).
|
|
40
|
+
- **Validación con `zodResolver`, no funciones por campo.** `validate: { email: (v) => … }` disemina reglas y tipos. Un schema Zod + `zodResolver` da una fuente única y el tipo por `z.infer`. Requiere el paquete `mantine-form-zod-resolver`.
|
|
41
|
+
- **Campos anidados/listas con notación de path.** `getInputProps('address.city')`, y listas con `form.insertListItem('items', {...})` / `form.removeListItem('items', i)` + `getInputProps('items.0.name')`. No manejes el array en `useState` aparte.
|
|
42
|
+
- **`initialValues` define el shape; llénalo completo.** Un campo ausente arranca `undefined` → warning uncontrolled→controlled. Para edición async usa `form.setValues(data)` / `form.initialize(data)` en un efecto, no valores a mano por render.
|
|
43
|
+
- **Submit con `form.onSubmit(handler)`.** Corre la validación y solo llama tu handler si pasa; además expone el segundo callback `(errors) => …` para enfocar el primer inválido. No valides "a mano" antes de enviar.
|
|
44
|
+
|
|
45
|
+
## Reglas duras
|
|
46
|
+
|
|
47
|
+
1. `useForm` es la única fuente del estado del form; nada de `useState` espejo.
|
|
48
|
+
2. Inputs cableados con `getInputProps`; no dupliques `value`/`onChange`.
|
|
49
|
+
3. Validación con schema Zod vía `zodResolver`; tipo por `z.infer`, sin reglas por campo.
|
|
50
|
+
4. `initialValues` completo; edición async con `setValues`/`initialize`.
|
|
51
|
+
5. Submit vía `form.onSubmit`; listas/anidados con la API de path, no arrays sueltos.
|
|
52
|
+
|
|
53
|
+
## Antes de declarar listo
|
|
54
|
+
|
|
55
|
+
- Todos los inputs usan `getInputProps`; sin estado espejo ni handlers duplicados.
|
|
56
|
+
- Validación centralizada en un schema Zod con `zodResolver`; tipos por `z.infer`.
|
|
57
|
+
- `initialValues` seteado; sin warnings uncontrolled→controlled. Submit con `form.onSubmit`.
|
|
58
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-router
|
|
3
|
+
description: Patrones de React Router (v6/v7) — rutas anidadas, loaders, navegación, params y guards. Aplica al crear rutas, leer params, redirigir o proteger vistas.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# React Router — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al tocar navegación: declarar una ruta, leer un param, redirigir, proteger una vista por rol, o cablear links. React Router es la fuente de verdad de **en qué URL estás y a dónde vas** — no dupliques la ruta en estado propio ni parsees `window.location` a mano.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
Rutas anidadas con layout compartido vía `<Outlet />`; navegación por hooks, no por mutar `window.location`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const router = createBrowserRouter([
|
|
19
|
+
{
|
|
20
|
+
path: '/',
|
|
21
|
+
element: <AppLayout />, // renderiza <Outlet /> para los hijos
|
|
22
|
+
children: [
|
|
23
|
+
{ index: true, element: <Home /> },
|
|
24
|
+
{ path: 'sessions/:id', element: <SessionDetail /> },
|
|
25
|
+
{ path: '*', element: <NotFound /> },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function SessionDetail() {
|
|
31
|
+
const { id } = useParams(); // string | undefined, siempre
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const [params, setParams] = useSearchParams();
|
|
34
|
+
// ...
|
|
35
|
+
navigate('/sessions', { replace: true }); // no <a href> manual
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Gotchas que muerden
|
|
40
|
+
|
|
41
|
+
- **`useParams()` siempre da `string | undefined`.** Nunca `number`. Convierte y valida (`Number(id)`, guard) antes de usarlo como id; una ruta mal tecleada no lanza, solo llega `undefined`.
|
|
42
|
+
- **Navegar imperativo con `useNavigate`, no `window.location`.** `window.location.href = …` recarga toda la SPA y tira el estado. Para volver: `navigate(-1)`; para redirigir sin dejar historial: `{ replace: true }`.
|
|
43
|
+
- **`<NavLink>` para tabs/menús, `<Link>` para el resto.** `NavLink` expone `isActive` en `className`/`style`/children; no reimplementes "está activo" comparando `pathname` a mano.
|
|
44
|
+
- **Search params son la URL, no `useState`.** Filtros/paginación viven en `useSearchParams` para que la vista sea linkeable y sobreviva al refresh. `setParams` reemplaza TODO el query — clona lo actual si solo cambias una clave.
|
|
45
|
+
- **Ruta protegida = un wrapper con `<Navigate>`, no un `if` suelto.** `if (!user) return <Navigate to="/login" replace />;` dentro de un guard/layout. Redirigir desde un `useEffect` parpadea la vista privada un frame.
|
|
46
|
+
- **Rutas relativas anidan; un `/` inicial las hace absolutas.** Dentro de `sessions/:id`, `navigate('edit')` va a `sessions/:id/edit`; `navigate('/edit')` va a la raíz. Es el error #1 al mover un componente de nivel.
|
|
47
|
+
|
|
48
|
+
## Reglas duras
|
|
49
|
+
|
|
50
|
+
1. Navegación por `useNavigate`/`<Link>`/`<NavLink>`; nunca `window.location` ni `<a href>` interno.
|
|
51
|
+
2. `useParams` se valida antes de usar (puede ser `undefined`); ids numéricos se convierten explícito.
|
|
52
|
+
3. Estado de filtros/paginación en `useSearchParams`, no en `useState` espejo.
|
|
53
|
+
4. Vistas protegidas por un guard con `<Navigate replace>`, no por `if` + efecto.
|
|
54
|
+
5. Layouts compartidos con rutas anidadas + `<Outlet />`; nada de repetir el chrome por página.
|
|
55
|
+
|
|
56
|
+
## Antes de declarar listo
|
|
57
|
+
|
|
58
|
+
- Sin `window.location`/`<a href>` para navegación interna; links con `<Link>`/`<NavLink>`.
|
|
59
|
+
- Params validados; el estado de la URL (filtros, tab) vive en search params.
|
|
60
|
+
- Rutas protegidas redirigen con `<Navigate replace>`; sin parpadeo de la vista privada.
|
|
61
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Stack — Monorepo (Turborepo + pnpm)
|
|
2
|
+
|
|
3
|
+
Este directorio es la **raíz de un monorepo**: orquesta, no aloja producto. El código real vive en los workspaces (`apps/*`, `packages/*`), y **cada workspace tiene su propio harness** (su `CLAUDE.md` + `.claude/`) con el preset de su stack. El mapa de workspaces vivos está en el bloque "## Monorepo — raíz".
|
|
4
|
+
|
|
5
|
+
Regla de oro: **enruta el trabajo al workspace dueño**. Un cambio de producto se hace desde el `CLAUDE.md` de su app, no desde aquí. La raíz solo se toca para lo transversal: `turbo.json`, `pnpm-workspace.yaml`, tsconfig/eslint base, scripts de CI, deps compartidas.
|
|
6
|
+
|
|
7
|
+
- **Tareas scopeadas, no globales.** Corre por workspace con el filtro de Turbo: `pnpm turbo run <task> --filter=<workspace>` (o `--filter=./apps/<x>`). Evita correr el pipeline entero cuando solo tocaste un app.
|
|
8
|
+
- **No cruces imports entre workspaces por ruta relativa** (`../../otro-app`). Consume un hermano por su nombre de paquete con el protocolo `workspace:*`; si no es un paquete publicable, probablemente el código debería vivir en un `packages/*` compartido.
|
|
9
|
+
- **La dep va en el `package.json` del workspace que la usa**, no en la raíz. Deps en la raíz son solo tooling del monorepo (turbo, changesets, linters compartidos).
|
|
10
|
+
|
|
11
|
+
Antes de tocar `turbo.json`, `pnpm-workspace.yaml` o mover deps entre workspaces, aplica el skill `turbo-workspaces`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: turbo-workspaces
|
|
3
|
+
description: Cómo navegar y operar un monorepo Turborepo + pnpm — correr tareas scopeadas, agregar deps al workspace correcto, compartir código sin acoplar. Aplica antes de tocar turbo.json, pnpm-workspace.yaml o mover deps.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Turborepo + pnpm — operación del monorepo
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Antes de: correr tareas de build/test/lint, agregar o mover una dependencia, crear un workspace nuevo, o editar `turbo.json` / `pnpm-workspace.yaml`. En un monorepo, "dónde" vive un cambio importa tanto como "qué" cambia.
|
|
12
|
+
|
|
13
|
+
## Correr tareas (siempre scopeadas)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Una tarea en UN workspace (por nombre de paquete o por ruta)
|
|
17
|
+
pnpm turbo run build --filter=@scope/backend
|
|
18
|
+
pnpm turbo run test --filter=./apps/storefront
|
|
19
|
+
|
|
20
|
+
# Un workspace y todo lo que depende de él (aguas abajo)
|
|
21
|
+
pnpm turbo run build --filter=@scope/backend...
|
|
22
|
+
|
|
23
|
+
# Solo lo afectado por tu diff vs una base
|
|
24
|
+
pnpm turbo run test --filter='...[origin/main]'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Regla: no corras el pipeline entero (`turbo run build`) si solo tocaste un app. Turbo cachea, pero el ruido de logs y el tiempo de arranque sí cuestan. Deja el run global para CI.
|
|
28
|
+
|
|
29
|
+
## Agregar dependencias (al workspace correcto)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Dep de un app concreto — NO en la raíz
|
|
33
|
+
pnpm add zod --filter @scope/backend
|
|
34
|
+
|
|
35
|
+
# Dep de tooling del monorepo (turbo, changesets, prettier) — esa sí va en la raíz
|
|
36
|
+
pnpm add -Dw turbo
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Una lib de producto (`stripe`, `@tanstack/react-query`, …) va en el `package.json` del app que la importa. Si aparece en la raíz, el harness de ese app no la "ve" y su skill no se materializa donde corresponde.
|
|
40
|
+
- Consumir un workspace hermano se declara explícito: `"@scope/ui": "workspace:*"` en el `package.json` del consumidor. Nunca por `import '../../ui/src/...'`.
|
|
41
|
+
|
|
42
|
+
## Compartir código sin acoplar
|
|
43
|
+
|
|
44
|
+
- Código usado por ≥2 apps → extráelo a un `packages/*` con su propio `package.json` y su preset (`navori scan` lo detecta como workspace nuevo).
|
|
45
|
+
- Tipos/utilidades cross-app también van en un `packages/*`, no en `apps/*`. Un app nunca es dependencia de otro app.
|
|
46
|
+
|
|
47
|
+
## turbo.json — lo esencial
|
|
48
|
+
|
|
49
|
+
- Cada `task` declara sus `dependsOn` (`^build` = build de las deps primero) y sus `outputs` (para cachear). Un output mal declarado = cache que no invalida o que no cachea.
|
|
50
|
+
- Antes de editar el pipeline, verifica el efecto con `pnpm turbo run <task> --dry-run` (lista qué correría y desde qué caché) antes de correrlo de verdad.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://navori.dev/schema/navori.preset.v1.json",
|
|
3
|
+
"id": "monorepo-turbopnpm",
|
|
4
|
+
"displayName": "Monorepo (Turborepo + pnpm)",
|
|
5
|
+
"extends": "core",
|
|
6
|
+
"extras": {
|
|
7
|
+
"managed": [
|
|
8
|
+
{
|
|
9
|
+
"id": "stack-monorepo-turbopnpm",
|
|
10
|
+
"relPath": "presets/monorepo-turbopnpm/managed/stack.md"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"agents": [],
|
|
14
|
+
"skills": [
|
|
15
|
+
{
|
|
16
|
+
"id": "turbo-workspaces",
|
|
17
|
+
"relPath": "presets/monorepo-turbopnpm/skills/turbo-workspaces.md",
|
|
18
|
+
"destRelPath": ".claude/skills/turbo-workspaces.md"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"hooks": []
|
|
22
|
+
}
|
|
23
|
+
}
|