🛠️ Modificar Código
- Editar componente/store/composable
- Hot reload automático en navegador
- Verificar en DevTools
- Continuar iterando
Objetivo: Dominar los workflows diarios para ser 100% productivo desarrollando microfrontends con nuestro stack.
# 1. Actualizar workspacecd proyecto-vuegit pull origin mainpnpm install
# 2. Build packages si hay cambiospnpm -r build
# 3. Iniciar desarrollopnpm app-{mi-proyecto} dev🛠️ Modificar Código
🧪 Verificar Cambios
📦 Preparar Commit
pnpm app-{proyecto} lint - Fix automáticopnpm app-{proyecto} build - Verificar buildgit add y git commit# Generar desde proyecto-vue (genera ejemplo funcional)cd proyecto-vue && git pullnpx plop # Seleccionar tipo según Decision Tree
# Copiar y configurar en workspace realCopy-Item -Recurse @apps/mi-nuevo-proyecto ../mi-workspace-real/@apps/cd ../mi-workspace-real# Agregar script: "app-{nombre}": "pnpm --filter {nombre}"pnpm install && pnpm -r build
# Ejecutar ejemplo generadopnpm app-{nombre} dev# → Verificar que funciona, luego adaptar a tu caso específicoPost-generación workflow típico:
# 1. Entender código generadocode @apps/{nombre}/src/
# 2. Adaptar global.ts a tu dominio# 3. Modificar store para tu API# 4. Adaptar componentes a tu UI# 5. Desarrollo iterativo normalEl template genera un store de ejemplo que puedes adaptar:
// stores/productStore.ts (generado por template)export const useProductStore = defineStore("products", { state: (): StoreState => ({ // ✅ Configuración inyectada (mantener) data: inject("configWindow") as Params | null, configEnv: inject("configEnv") as ConfigEnv,
// ✅ Loading states (mantener pattern) isLoading: false,
// 🔄 ADAPTAR: Cambiar products por tu dominio flights: [], // Era: products: [] selectedFlight: null, // Era: selectedProduct: null error: null, }),
getters: { // ✅ Mantener getters de configuración currency: (state): string => state.data?.config.currency ?? "USD",
// 🔄 ADAPTAR: Cambiar lógica específica availableFlights: (state): Flight[] => state.flights.filter((flight) => flight.available), },
actions: { // ✅ Mantener pattern de async actions async fetchFlights(): Promise<void> { // Era: fetchProducts this.isLoading = true; this.error = null;
try { // 🔄 ADAPTAR: Tu API específica const response = await apiClient.get(`/flights/${this.searchId}`); if (response) { this.flights = response; // Era: this.products } } catch (error) { this.error = error instanceof Error ? error.message : "Unknown error"; } finally { this.isLoading = false; } }, },});import { ref } from "vue";import type { ApiResponse } from "@pnpmworkspace/types";
export function useApiClient(baseUrl: string) { const isLoading = ref(false); const error = ref<string | null>(null);
async function get<T>(endpoint: string): Promise<T | null> { isLoading.value = true; error.value = null;
try { const response = await fetch(`${baseUrl}${endpoint}`); const data: ApiResponse<T> = await response.json();
if (data.success) { return data.data || null; } else { error.value = data.error || "API Error"; return null; } } catch (err) { error.value = err instanceof Error ? err.message : "Network Error"; return null; } finally { isLoading.value = false; } }
return { isLoading: readonly(isLoading), error: readonly(error), get, };}<script setup lang="ts">import { reactive } from "vue";import { useValidation } from "@/composables/useValidation";
interface FormData { name: string; email: string; age: number | null;}
const formData = reactive<FormData>({ name: "", email: "", age: null,});
const { errors, validate, isValid } = useValidation(formData, { name: { required: true, minLength: 2 }, email: { required: true, email: true }, age: { required: true, min: 18 },});
async function handleSubmit(): Promise<void> { if (!validate()) return;
// Submit logic await submitForm(formData);}</script>
<template> <form @submit.prevent="handleSubmit"> <div class="field"> <label for="name">Nombre</label> <input id="name" v-model="formData.name" :class="{ error: errors.name }" type="text" /> <span v-if="errors.name" class="error-text" >{{ errors.name }}</span > </div>
<button type="submit" :disabled="!isValid" > {{ $t("common.submit") }} </button> </form></template>import { defineStore } from "pinia";import { inject } from "vue";import type { User, ConfigEnv } from "@pnpmworkspace/types";
interface UserState { // Configuración configEnv: ConfigEnv;
// Estados de loading específicos isLoading: boolean; isUpdating: boolean;
// Datos principales currentUser: User | null; users: User[];
// Errores específicos fetchError: string | null; updateError: string | null;}
export const useUserStore = defineStore("user", { state: (): UserState => ({ configEnv: inject("configEnv") as ConfigEnv,
isLoading: false, isUpdating: false,
currentUser: null, users: [],
fetchError: null, updateError: null, }),
getters: { // Estados agregados hasAnyError: (state): boolean => Boolean(state.fetchError || state.updateError),
isAnyLoading: (state): boolean => state.isLoading || state.isUpdating,
// Transformaciones de negocio activeUsers: (state): User[] => state.users.filter((user) => user.isActive),
userById: (state) => (id: string): User | undefined => state.users.find((user) => user.id === id), },
actions: { // Limpieza de errores clearErrors(): void { this.fetchError = null; this.updateError = null; },
// Fetching con error handling async fetchUsers(): Promise<void> { this.isLoading = true; this.fetchError = null;
try { const response = await apiClient.get<User[]>("/users"); if (response) { this.users = response; } } catch (error) { this.fetchError = error instanceof Error ? error.message : "Unknown error"; } finally { this.isLoading = false; } },
// Updating con optimistic updates async updateUser(userId: string, updates: Partial<User>): Promise<boolean> { this.isUpdating = true; this.updateError = null;
// Optimistic update const originalUser = this.userById(userId); if (originalUser) { Object.assign(originalUser, updates); }
try { const response = await apiClient.put<User>(`/users/${userId}`, updates); if (response) { // Confirm with server response const index = this.users.findIndex((u) => u.id === userId); if (index !== -1) { this.users[index] = response; } return true; } return false; } catch (error) { // Revert optimistic update if (originalUser) { const index = this.users.findIndex((u) => u.id === userId); if (index !== -1) { this.users[index] = originalUser; } } this.updateError = error instanceof Error ? error.message : "Update failed"; return false; } finally { this.isUpdating = false; } },
// Inicialización async initialize(): Promise<void> { this.clearErrors(); await this.fetchUsers(); }, },});<script setup lang="ts">import { storeToRefs } from "pinia";import { useUserStore } from "@/stores/userStore";
const userStore = useUserStore();
// ✅ Correcto: storeToRefs mantiene reactividadconst { users, isLoading, hasAnyError } = storeToRefs(userStore);
// ✅ Correcto: actions se pueden desestructurar directamenteconst { fetchUsers, updateUser } = userStore;
// ❌ Incorrecto: perdería reactividad// const { users, isLoading } = userStore;</script>
<template> <div> <button @click="fetchUsers()" :disabled="isLoading" > {{ $t("common.refresh") }} </button>
<div v-if="hasAnyError" class="error" > {{ $t("errors.general") }} </div>
<UserList :users="users" :loading="isLoading" @update="updateUser" /> </div></template>📁 public/locales/├── 📁 en-US/│ └── 📄 translation.json├── 📁 es-MX/│ └── 📄 translation.json└── 📁 es-CO/ └── 📄 translation.json{ "common": { "loading": "Loading...", "error": "Error", "retry": "Try Again", "submit": "Submit", "cancel": "Cancel" }, "products": { "title": "Products", "addToCart": "Add to Cart", "outOfStock": "Out of Stock", "price": "Price: {{amount}} {{currency}}" }, "errors": { "network": "Network error. Please check your connection.", "validation": { "required": "This field is required", "email": "Please enter a valid email", "minLength": "Minimum {{min}} characters required" } }}<template> <div> <!-- Traducción simple --> <h1>{{ $t("products.title") }}</h1>
<!-- Con interpolación --> <p>{{ $t("products.price", { amount: 29.99, currency: "USD" }) }}</p>
<!-- Pluralización --> <span>{{ $t("common.items", { count: itemCount }) }}</span>
<!-- Conditional --> <button v-if="inStock"> {{ $t("products.addToCart") }} </button> <span v-else>{{ $t("products.outOfStock") }}</span> </div></template><script setup lang="ts">import { useI18n } from "i18next-vue";
const { t } = useI18n();
// Para uso programáticoconst dynamicMessage = computed(() => t("products.price", { amount: product.value.price, currency: product.value.currency, }),);
function showErrorMessage(error: string) { alert(t("errors.network"));}</script>import { useI18n } from "i18next-vue";import { ref } from "vue";
export function useLanguage() { const { i18n } = useI18n(); const currentLanguage = ref(i18n.language);
async function changeLanguage(lang: string): Promise<void> { await i18n.changeLanguage(lang); currentLanguage.value = lang;
// Guardar preferencia localStorage.setItem("user-language", lang); }
return { currentLanguage: readonly(currentLanguage), changeLanguage, };}🔧 Vue DevTools
Instalación: Extension de Chrome/Firefox
Uso diario:
🗃️ Pinia DevTools
Incluido en Vue DevTools Uso diario: - Estado de todos los stores - Timeline de mutations
⚡ Vite DevTools
Automático en desarrollo
Uso diario:
❌ “Cannot find module ‘@pnpmworkspace/…’”
# 1. Verificar que packages están builtpnpm -r build
# 2. Restart TypeScript server en VS Code# Ctrl+Shift+P → "TypeScript: Restart TS Server"
# 3. Limpiar cache si persisteRemove-Item node_modules/.vite -Recurse -Force❌ “Type errors en tiempo real”
# Verificar que usa workspace TypeScript# VS Code: Ctrl+Shift+P → "TypeScript: Select Version" → "Use Workspace Version"❌ “ERR_PNPM_OUTDATED_LOCKFILE”
# Actualizar lockfilepnpm install --frozen-lockfile=false
# O forzar reinstall completoRemove-Item pnpm-lock.yamlpnpm install❌ “Port already in use”
# Encontrar proceso que usa el puertonetstat -ano | findstr :5173
# Matar proceso (reemplazar PID)taskkill /PID 1234 /F
# O usar puerto diferentepnpm app-{proyecto} dev --port 5174❌ “Module not found después de git pull”
# Workflow completo post-pullgit pullpnpm installpnpm -r buildpnpm app-{proyecto} dev❌ “Build fails silently”
# Build con verbose outputpnpm app-{proyecto} builddev
# Verificar logs específicospnpm app-{proyecto} lint❌ “Hot reload no funciona”
# Restart con host bindingpnpm app-{proyecto} dev --host
# Verificar que no hay errores TypeScript# VS Code: Ver panel "Problems"❌ “Chunks no se cargan en producción”
# Verificar configuración CDN en .env# VITE_CDN_BASE_URL debe coincidir con deploy// Debug helpers para Pinia storesexport const useDebugStore = defineStore("debug", { actions: { logAllStores() { // En development only if (import.meta.env.DEV) { console.group("🗃️ Pinia Stores State"); // Iterar todos los stores y loggear estado console.groupEnd(); } },
logApiCalls() { // Interceptar y loggear llamadas de API if (import.meta.env.DEV) { console.log("📡 API Call:", arguments); } }, },});<script setup lang="ts">import { defineAsyncComponent } from "vue";
// ✅ Lazy load componentes pesadosconst HeavyChart = defineAsyncComponent(() => import("@/components/HeavyChart.vue"));const DataTable = defineAsyncComponent(() => import("@/components/DataTable.vue"));
const showChart = ref(false);</script>
<template> <div> <button @click="showChart = !showChart">{{ showChart ? "Hide" : "Show" }} Chart</button>
<!-- Solo carga cuando se necesita --> <Suspense v-if="showChart"> <HeavyChart :data="chartData" /> <template #fallback> <div class="loading">Loading chart...</div> </template> </Suspense> </div></template><script setup lang="ts">import { computed, shallowRef } from "vue";
// ✅ Para arrays grandes, usar shallowRefconst largeDataset = shallowRef<Item[]>([]);
// ✅ Memoization manual para cálculos costososconst expensiveComputation = computed(() => { console.log("🔄 Recalculating expensive computation"); return largeDataset.value .filter((item) => item.isActive) .sort((a, b) => b.priority - a.priority) .slice(0, 100); // Solo top 100});
// ✅ Computed dependientes específicosconst filteredCount = computed(() => expensiveComputation.value.length);</script># Analizar tamaño de buildpnpm app-{proyecto} build
# Ver breakdown de chunksGet-ChildItem dist/ | Where-Object { $_.Extension -match '\.(js|css)$' }
# Para análisis detallado (opcional)# npm install -g bundlemon# bundlemonpnpm app-{proyecto} lint sin errorespnpm app-{proyecto} build exitosopnpm -r build)Ahora que dominas el desarrollo día a día: