🌱 CULTIVA IA · Skill: migracion-codigo

Plan de Migración de Código

AgroSmart SaaS — JavaScript + Class Components → TypeScript + Hooks + React Router v6

Archivos 34 ficheros
Líneas ~4.200 LOC
Sprints 3–4 sprints
Riesgo Controlado
Estado actual de la migración
Archivos migrados
14 / 34
41% completado
Type errors
23
Reducidos de 87 iniciales
Tests en verde
22 / 22
100% sin regresiones
Interfaces creadas
11
Sensor, Farm, Alert, User…
Fases del plan de migración
1
Setup inicial (no-breaking)
Configuración de TypeScript con allowJs:true y tsconfig base
✓ Completada
Instalar TypeScript 5.x, @types/react, @types/node, @types/react-router-dom
Crear tsconfig.json con strict:false y allowJs:true
Configurar Vite para soportar .ts/.tsx en paralelo a .js/.jsx
Renombrar entry point: main.js → main.ts
Añadir @types/leaflet para FarmMap (usa Leaflet via ref)
2
Migración incremental de archivos
JS → TS, class components → hooks — un módulo a la vez
⏳ En curso
src/utils/api.js → api.ts (con interfaces Sensor, Farm, Alert)
src/utils/date.js → date.ts
src/components/SensorDashboard.jsx → SensorDashboard.tsx (class → useSensor hook)
src/components/FarmMap.jsx → FarmMap.tsx (useRef + leaflet types)
src/components/AlertList.jsx → AlertList.tsx
src/pages/*.jsx → *.tsx (12 archivos)
3
React Router v5 → v6
Switch/Route/Redirect/useHistory → Routes/Route/Navigate/useNavigate
Pendiente
npm install react-router-dom@6 · eliminar v5
Reemplazar <Switch> → <Routes> en App.tsx
Transformar component= → element= (9 rutas)
useHistory() → useNavigate() (6 archivos)
<Redirect> → <Navigate replace>
4
Modo estricto y limpieza final
Activar strict:true, eliminar any, añadir tipos de retorno
Pendiente
Activar strict:true en tsconfig.json
Resolver todos los `any` explícitos con tipos reales
Añadir anotaciones de retorno a todas las funciones públicas
npx tsc --noEmit → 0 errores
npm run build → build limpio para producción
Transformación 1 — src/utils/api.js → api.ts
ANTES — api.js (JavaScript sin tipos)
// src/utils/api.js — sin interfaces
const fetchSensors = async (farmId) => {
  const res = await fetch(
    `/api/farms/${farmId}/sensors`
  );
  if (!res.ok)
    throw new Error('Error al obtener sensores');
  return res.json();
  // ⚠ devuelve `any`
};

const updateSensor = async (id, data) => {
  const res = await fetch(`/api/sensors/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  return res.json();
};

module.exports = { fetchSensors, updateSensor };
DESPUÉS — api.ts (TypeScript con interfaces)
// src/utils/api.ts — tipado completo
interface Sensor {
  id: string; farmId: string;
  type: 'humidity' | 'temperature' | 'ph';
  value: number; unit: string;
  lastSeen: string; active: boolean;
}

type SensorPatch = Partial<Pick<Sensor, 'value' | 'active'>>;

export const fetchSensors = async (
  farmId: string
): Promise<Sensor[]> => {
  const res = await fetch(
    `/api/farms/${farmId}/sensors`
  );
  if (!res.ok)
    throw new Error('Error al obtener sensores');
  return res.json() as Promise<Sensor[]>;
};

export const updateSensor = async (
  id: string, data: SensorPatch
): Promise<Sensor> => {
  const res = await fetch(`/api/sensors/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  return res.json() as Promise<Sensor>;
};
Transformación 2 — SensorDashboard: class component → hook
ANTES — Class Component (React 16 pattern)
// SensorDashboard.jsx
class SensorDashboard
  extends React.Component {

  state = {
    sensors: [],
    loading: true,
    error: null,
  };

  componentDidMount() {
    this.loadSensors();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.farmId !== this.props.farmId)
      this.loadSensors();
  }

  loadSensors() {
    this.setState({ loading: true });
    fetchSensors(this.props.farmId)
      .then(sensors =>
        this.setState({ sensors, loading: false })
      )
      .catch(err =>
        this.setState({ error: err.message })
      );
  }

  render() {
    const { loading, sensors } = this.state;
    if (loading) return <Spinner />;
    return <SensorList sensors={sensors} />;
  }
}
DESPUÉS — Functional + Custom Hook (TypeScript)
// hooks/useSensors.ts — hook extraído
function useSensors(farmId: string) {
  const [sensors, setSensors] =
    useState<Sensor[]>([]);
  const [loading, setLoading] =
    useState(true);
  const [error, setError] =
    useState<string | null>(null);

  useEffect(() => {
    setLoading(true);
    fetchSensors(farmId)
      .then(setSensors)
      .catch(e => setError(e.message))
      .finally(() => setLoading(false));
  }, [farmId]);

  return { sensors, loading, error };
}

// SensorDashboard.tsx — componente limpio
function SensorDashboard({
  farmId,
}: { farmId: string }) {
  const { sensors, loading, error } =
    useSensors(farmId);

  if (loading) return <Spinner />;
  if (error)
    return <ErrorBanner msg={error} />;
  return <SensorList sensors={sensors} />;
}
Seguimiento de archivos
Archivo Tipo migración Riesgo Type errors corregidos Estado
src/utils/api.js JS → TS + interfaces Bajo 12 ✓ Listo
src/utils/date.js JS → TS Bajo 3 ✓ Listo
src/components/SensorDashboard.jsx Class → hooks + TS Medio 18 ✓ Listo
src/components/FarmMap.jsx Class → hooks (Leaflet ref) Alto ⏳ WIP
src/components/AlertList.jsx Class → hooks + TS Bajo Pendiente
src/pages/Dashboard.jsx JS → TS + React Router v6 Medio Pendiente
src/pages/Settings.jsx JS → TS + useNavigate Bajo Pendiente
src/App.jsx Switch → Routes (v6) Alto Pendiente
Comandos de verificación (ejecutar tras cada fichero)
$ npx tsc --noEmit # type-check sin compilar
$ npm test # 22 tests Vitest — deben pasar al 100%
$ npx eslint src/ # 0 errores ESLint
$ npm run build # Vite build — 0 warnings críticos
Resultado esperado al finalizar
34
archivos migrados (34/34)
0
type errors (strict: true)
22/22
tests pasando · 0 regresiones