Las tres lecciones anteriores han resuelto cómo un componente Lit se integra con distintos consumidores: HTML plano, otros frameworks, y el propio servidor antes de que el navegador entre en juego. Esta lección cierra el módulo con la pregunta que queda una vez que un componente —o una aplicación completa como TaskFlow— está listo para salir del entorno de desarrollo: cómo empaquetarlo, cómo decidir si se publica como librería reutilizable o se despliega como aplicación, y si conviene o no migrar el código JavaScript escrito durante todo el curso a TypeScript.

Contenido

  1. Dos objetivos distintos: publicar una librería frente a desplegar una aplicación
  2. Bundlers para componentes Lit: Rollup, Vite y esbuild
  3. Empaquetar <task-card> como librería de componentes con Rollup
  4. Desplegar TaskFlow como aplicación con Vite
  5. El tamaño del bundle: por qué Lit es deliberadamente pequeño
  6. Migrar a TypeScript: decoradores frente a static properties
  7. Equivalencia 1:1: qué cambia y qué no cambia
  8. Cierre del módulo 8

  1. Dos objetivos distintos: publicar una librería frente a desplegar una aplicación

Antes de hablar de herramientas concretas, conviene distinguir dos escenarios que, aunque comparten vocabulario ("empaquetar", "build"), persiguen objetivos distintos y por tanto requieren configuraciones distintas. El primer escenario es publicar una librería de componentes: por ejemplo, si <task-card> y <user-avatar> se consideraran suficientemente genéricos y útiles como para publicarse en npm y que otros proyectos —no solo TaskFlow— los instalen como dependencia. En ese caso, el objetivo del empaquetado es producir un módulo que otros bundlers puedan, a su vez, importar y volver a empaquetar dentro de sus propios proyectos, típicamente sin incluir Lit dentro del propio paquete (dejando que quien lo consuma aporte su propia copia de Lit como dependencia compartida).

El segundo escenario es desplegar una aplicación completa: TaskFlow en su conjunto, con todos sus componentes ya combinados, listo para servirse directamente a los usuarios finales desde un servidor web o una red de distribución de contenido (CDN). Aquí el objetivo es justamente el contrario en un aspecto clave: el bundle final debe incluir todo lo necesario para funcionar por sí mismo —incluido Lit— optimizado para el navegador (código minificado, división en fragmentos cargados solo cuando hacen falta, gestión de caché por nombre de fichero con hash), sin esperar que quien la visite tenga que resolver ninguna dependencia adicional por su cuenta.

Esta distinción determina, en gran medida, qué herramienta conviene en cada caso, tal como se ve en los dos apartados siguientes.

  1. Bundlers para componentes Lit: Rollup, Vite y esbuild

El propio equipo de Lit recomienda Rollup como bundler de referencia para publicar librerías de componentes, y esa recomendación no es arbitraria: Rollup está diseñado, desde su origen, con la producción de librerías como caso de uso principal, generando un código de salida limpio y cercano al de entrada, sin el tipo de fragmentación orientada a aplicaciones de página completa que otros bundlers priorizan. Herramientas oficiales del propio ecosistema de Lit, como el generador de proyectos @lit-labs/starter-kit (o su equivalente vigente en el momento de crear un proyecto nuevo), configuran Rollup por defecto precisamente para este propósito.

Vite, el bundler usado durante todo este curso para el proyecto de desarrollo de TaskFlow, es la alternativa natural para el segundo escenario del apartado anterior: desplegar una aplicación completa. Vite usa, internamente, Rollup para su propio proceso de construcción de producción (vite build), pero añade por encima toda la experiencia de desarrollo que ha acompañado al curso —servidor con recarga en caliente, resolución rápida de módulos— y una configuración por defecto ya orientada a aplicaciones, no a librerías: divide el código en fragmentos, optimiza recursos estáticos, y genera un resultado listo para servir directamente sin pasos adicionales.

esbuild aparece con frecuencia como una tercera opción, valorada sobre todo por su velocidad de construcción, notablemente superior a la de Rollup o a la de bundlers más antiguos, gracias a estar escrito en Go en lugar de JavaScript. Vite, de hecho, usa esbuild internamente para tareas de transformación en su servidor de desarrollo (aunque recurra a Rollup para el build de producción final), y esbuild puede usarse también de forma directa como bundler independiente para casos donde la velocidad de construcción importa más que el control fino sobre la salida que ofrece Rollup.

Bundler Mejor encaje Motivo
Rollup Publicar una librería de componentes Salida limpia, cercana al código fuente, pensada para que otros bundlers la vuelvan a procesar
Vite Desplegar una aplicación completa Experiencia de desarrollo ya usada durante el curso, más una configuración de producción orientada a aplicaciones (usa Rollup por debajo)
esbuild Casos donde la velocidad de construcción es prioritaria Mucho más rápido que alternativas basadas en JavaScript, a costa de algo menos de control fino sobre la salida

  1. Empaquetar <task-card> como librería de componentes con Rollup

Si <task-card> se extrajera de TaskFlow para publicarse como paquete npm independiente, una configuración mínima de Rollup podría verse así:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/task-card.js',
  output: {
    file: 'dist/task-card.js',
    format: 'esm',
  },
  plugins: [resolve()],
};

input señala el punto de entrada —el propio fichero del componente—, y output.format: 'esm' indica que la salida debe seguir siendo un módulo ES, el formato que el propio Lit y el resto del ecosistema moderno de JavaScript esperan poder importar. El plugin @rollup/plugin-node-resolve permite que Rollup resuelva correctamente el import { LitElement, html, css } from 'lit' del propio componente, localizando el paquete lit dentro de node_modules, exactamente el mismo tipo de resolución de módulos que Vite hace de forma transparente durante todo el curso, pero que Rollup necesita configurar explícitamente a través de un plugin.

Un detalle importante para este escenario: por defecto, esta configuración no incluye el código de Lit dentro del bundle resultante, dado que lit aparece declarado como dependencia (no incluida directamente) en el package.json del paquete a publicar; quien instale task-card desde npm instalará también lit como dependencia transitiva, y su propio bundler será quien decida cómo combinarlas, evitando que dos componentes distintos de dos paquetes distintos, ambos dependientes de Lit, terminen incluyendo dos copias separadas e independientes de la misma biblioteca en la aplicación final que los consuma a ambos.

  1. Desplegar TaskFlow como aplicación con Vite

Para TaskFlow como aplicación completa, el comando de construcción de producción de Vite, ya configurado por el propio proyecto creado al inicio del curso, resuelve el escenario contrario:

npm run build

Este comando ejecuta, por debajo, vite build, que usa Rollup para generar un directorio dist/ con el resultado final: los ficheros JavaScript minificados, con nombres que incluyen un hash del contenido (útil para el control de caché del navegador, ya que un cambio de contenido produce automáticamente un nombre de fichero distinto), las hojas de estilo extraídas si corresponde, y un index.html ya apuntando a esos ficheros generados. A diferencia del escenario de librería del apartado anterior, aquí sí interesa incluir Lit dentro del propio bundle final: TaskFlow, como aplicación, no tiene ningún consumidor externo que aporte su propia copia de Lit; el bundle final debe ser autosuficiente, listo para copiarse a cualquier servidor de ficheros estáticos o CDN y funcionar sin ninguna dependencia adicional que resolver en tiempo de ejecución.

  1. El tamaño del bundle: por qué Lit es deliberadamente pequeño

La lección 01-01 ya mencionó el tamaño reducido de Lit como una de sus ventajas frente a un framework de aplicación completo, y este es el momento del curso en que esa cifra cobra importancia práctica real: cuanto menor sea el tamaño de la propia biblioteca incluida en el bundle final, menos tiempo tarda el navegador en descargarla, analizarla y ejecutarla antes de poder definir el primer componente, un factor que incide directamente en la ventana de "página vacía" descrita en la lección anterior sobre SSR, incluso en aplicaciones que no usan SSR en absoluto.

Lit consigue su tamaño reducido —unos pocos kilobytes comprimidos con gzip, muy por debajo de frameworks de aplicación completos— por una decisión de diseño deliberada, no por casualidad: Lit se limita, de forma consciente, a resolver el problema de las plantillas reactivas y las actualizaciones eficientes del DOM sobre Custom Elements estándar, sin incluir un enrutador propio, sin un sistema de gestión de estado global propio, sin las decenas de utilidades adicionales que un framework de aplicación completo suele incorporar por defecto. Cada una de esas piezas ausentes —enrutamiento, estado global— es, precisamente, terreno que TaskFlow ha resuelto durante el curso con herramientas propias y minimalistas (@lit/context para estado compartido, en lugar de una librería de gestión de estado global de propósito general), en lugar de depender de que Lit las incluyera de serie. Un bundler como Rollup o Vite puede, además, aplicar tree-shaking —eliminar del bundle final cualquier parte de una biblioteca que el código de la aplicación no llegue a usar en ningún punto—, y el propio diseño modular de Lit (funciones y clases exportadas de forma independiente, en lugar de un único objeto monolítico que lo incluya todo) favorece que ese tree-shaking sea efectivo, reduciendo aún más el tamaño final para aplicaciones que solo usan un subconjunto de las funcionalidades disponibles.

  1. Migrar a TypeScript: decoradores frente a static properties

Todo el código de TaskFlow, durante las siete lecciones y media de curso anteriores a este módulo, se ha escrito en JavaScript puro, con static properties como forma de declarar propiedades reactivas. Lit ofrece, de forma opcional y sin ninguna obligación de adoptarla, una sintaxis alternativa basada en decoradores de TypeScript (o de JavaScript con la propuesta de decoradores habilitada mediante Babel), pensada para quien prefiera —o cuyo proyecto ya use— TypeScript como lenguaje principal:

// Versión con static properties (JavaScript, la usada durante todo el curso)
import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    expandida: { state: true },
  };

  constructor() {
    super();
    this.expandida = false;
  }

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}
// Versión con decoradores (TypeScript)
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('task-card')
class TaskCard extends LitElement {
  @property({ type: String })
  titulo = '';

  @property({ type: String })
  estado = '';

  @state()
  private expandida = false;

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}

Tres decoradores cubren, en conjunto, lo mismo que static properties más customElements.define(...) resuelven en la versión de JavaScript: @customElement('task-card') sustituye directamente a la llamada customElements.define('task-card', TaskCard) colocada al final de cada fichero de componente durante todo el curso, registrando la clase con ese nombre de etiqueta en el mismo punto donde se declara la clase, en lugar de en una línea aparte al final del fichero; @property({...}) sobre un campo de la clase declara esa propiedad como reactiva y pública, con las mismas opciones de configuración (type, attribute, converter, reflect) ya estudiadas desde la lección 03-01 para el objeto de configuración de static properties; y @state() sustituye a la opción { state: true }, marcando la propiedad como estado interno reactivo, no expuesto como atributo público (tal como se explicó en la lección 03-02).

  1. Equivalencia 1:1: qué cambia y qué no cambia

El punto más importante de este apartado, y el motivo por el que esta lección puede presentar TypeScript sin necesidad de repetir nada del resto del curso, es que la migración a decoradores es puramente sintáctica: no cambia ningún concepto de Lit estudiado hasta ahora, solo la forma de escribirlo. El ciclo de renderizado (módulo 2), el sistema de propiedades reactivas y su ciclo de actualización (módulo 3), los estilos con Shadow DOM (módulo 4), la comunicación entre componentes (módulo 5), el ciclo de vida, los controladores reactivos y los mixins (módulo 6), y las directivas incluido @lit/context (módulo 7) funcionan exactamente igual, con el mismo comportamiento en tiempo de ejecución, tanto si el componente se declaró con static properties como con decoradores. Un ReactiveController como ContadorTiempoRestanteController (lección 06-03) no necesita ninguna adaptación para usarse desde un componente escrito con decoradores; una directiva personalizada como resaltarSiUrgente (lección 07-02) tampoco.

Lo que sí cambia es exclusivamente la forma de declarar la clase y sus propiedades:

Aspecto JavaScript (static properties) TypeScript (decoradores)
Registrar el elemento customElements.define('task-card', TaskCard) al final del fichero @customElement('task-card') justo encima de la clase
Declarar una propiedad pública Entrada en el objeto static properties @property({...}) sobre el campo correspondiente
Declarar estado interno { state: true } dentro de static properties @state() sobre el campo correspondiente
Valor inicial de una propiedad Asignación en el constructor Asignación directa en la declaración del campo (titulo = '')
Comprobación de tipos Ninguna en tiempo de compilación; solo la conversión de tipo en tiempo de ejecución (módulo 3) TypeScript comprueba, en tiempo de compilación, que el código use cada propiedad con el tipo declarado

Adoptar TypeScript y los decoradores es, por tanto, una decisión que se puede tomar de forma independiente para cada proyecto —o incluso migrar progresivamente, componente a componente, dado que ambas sintaxis pueden convivir dentro de una misma aplicación mientras dure la transición—, sin que suponga reescribir ninguna de las decisiones de diseño ya tomadas durante el curso; lo único que cambia es la forma en que esas mismas decisiones se expresan en el código, más el beneficio añadido de la comprobación de tipos que TypeScript aporta por encima, ajena por completo a Lit en sí mismo.

  1. Cierre del módulo 8

Con esta lección se completa el módulo 8. El recorrido ha ido de dentro hacia fuera: primero, cómo un componente Lit se usa directamente en HTML plano gracias a ser, en el fondo, un Custom Element estándar; después, cómo se integra dentro de aplicaciones construidas con otros frameworks —React, Vue y Angular—, cada uno con sus propios puntos de fricción al tratar con Custom Elements; a continuación, cómo se renderiza en el servidor con @lit-labs/ssr y Declarative Shadow DOM, para que el contenido esté visible antes incluso de que el JavaScript se ejecute; y, por último, cómo se empaqueta y publica —como librería con Rollup, o como aplicación desplegable con Vite— y qué cambia, o más bien qué no cambia, al migrar opcionalmente a TypeScript.

Errores Comunes y Consejos

  • Incluir Lit dentro del bundle de una librería de componentes pensada para publicarse en npm: como se explicó en el apartado 3, esto obliga a cualquier consumidor que use varios componentes de distintos paquetes, todos dependientes de Lit, a cargar varias copias independientes de la misma biblioteca; declarar lit como dependencia externa, no incluida en el propio bundle, evita ese problema.
  • Usar la configuración de Vite pensada para aplicaciones al publicar una librería, o viceversa: como se ha visto en los apartados 1 y 2, ambos escenarios tienen objetivos distintos (autosuficiencia frente a reutilización externa por otros bundlers); usar la herramienta equivocada para el escenario equivocado suele traducirse en un bundle de librería innecesariamente grande, o en una aplicación desplegada que sigue esperando que el navegador resuelva dependencias por su cuenta.
  • Mezclar static properties y decoradores dentro de la misma clase: aunque ambas sintaxis pueden convivir en proyectos distintos, o incluso en componentes distintos de un mismo proyecto en migración progresiva (apartado 7), combinar ambos estilos dentro de una única clase suele generar confusión sobre qué mecanismo está realmente declarando cada propiedad, y conviene evitarlo dentro de un mismo fichero.
  • Adoptar TypeScript esperando que cambie el comportamiento en tiempo de ejecución de algún concepto de Lit: como se ha insistido en el apartado 7, la migración es puramente sintáctica; si un componente tenía un problema de diseño en JavaScript (por ejemplo, olvidar propagar Base.properties en un mixin, como se advirtió en la lección 06-04), ese mismo problema puede reaparecer en TypeScript si no se corrige explícitamente, ya que los decoradores no sustituyen ni corrigen automáticamente ninguna de las prácticas ya estudiadas durante el curso.

Ejercicios

  1. Reescribe la clase TaskFilter de la lección 07-04 (con su ContextConsumer y sus métodos manejarTexto/manejarEstado) usando decoradores de TypeScript en lugar de static properties, manteniendo exactamente el mismo comportamiento.
  2. Explica, basándote en el apartado 3, qué problema concreto evita declarar lit como dependencia externa (no incluida en el bundle) al publicar <task-card> como paquete npm independiente, en un escenario en el que una aplicación consumidora usara también otro componente de un tercero, igualmente dependiente de Lit.
  3. Un compañero de equipo, tras leer sobre Rollup y Vite en el apartado 2, propone usar Rollup también para el build de producción de la propia aplicación TaskFlow, en lugar de vite build, argumentando que "es la herramienta recomendada por el equipo de Lit". Explica por qué ese razonamiento no es correcto para el caso concreto de TaskFlow, apoyándote en la distinción del apartado 1.

Soluciones

import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';

@customElement('task-filter')
class TaskFilter extends LitElement {
  private _filtro: ContextConsumer<typeof filtroContext, this>;

  constructor() {
    super();
    this._filtro = new ContextConsumer(this, { context: filtroContext, subscribe: true });
  }

  get valorActual() {
    return this._filtro.value ?? { texto: '', estado: 'todas', actualizar: () => {} };
  }

  manejarTexto(evento: Event) {
    this.valorActual.actualizar({ texto: (evento.target as HTMLInputElement).value });
  }

  manejarEstado(estado: string) {
    this.valorActual.actualizar({ estado });
  }

  render() {
    const { texto, estado } = this.valorActual;
    return html`
      <div class="filtro">
        <input
          type="text"
          placeholder="Buscar tarea…"
          .value="${texto}"
          @input="${this.manejarTexto}"
        />
        <div class="filtro__botones">
          ${['todas', 'pendiente', 'hecha'].map(
            (opcion) => html`
              <button
                class="${classMap({ activo: estado === opcion })}"
                @click="${() => this.manejarEstado(opcion)}"
              >
                ${{ todas: 'Todas', pendiente: 'Pendientes', hecha: 'Hechas' }[opcion]}
              </button>
            `
          )}
        </div>
      </div>
    `;
  }

  static styles = css`
    .filtro__botones button.activo {
      font-weight: bold;
      border-bottom: 2px solid currentColor;
    }
  `;
}

No hay ningún @property ni @state en esta clase porque, igual que en la versión original de la lección 07-04, TaskFilter no declara ninguna propiedad reactiva propia: todo su estado visible (texto, estado) se lee directamente de this._filtro.value en cada render(), exactamente igual que en la versión en JavaScript.

  1. Si <task-card> incluyera su propia copia de Lit dentro del bundle publicado en npm, y un componente de un tercero (por ejemplo, un <other-widget> de otro paquete, también construido con Lit) hiciera lo mismo, una aplicación que usara ambos terminaría cargando dos copias completas e independientes de Lit en el navegador del usuario final: cada una con su propio código, su propio consumo de memoria, y —más grave todavía— sin ninguna garantía de que ambas copias reconozcan las mismas instancias de contexto (@lit/context) o de directivas si, en algún punto, ambos componentes necesitaran compartir alguna de esas piezas de infraestructura entre sí. Declarando lit como dependencia externa del paquete, el bundler de la aplicación consumidora resuelve una única copia compartida de Lit, instalada una sola vez en el árbol de dependencias, y ambos componentes de terceros la reutilizan sin duplicación.
  2. La recomendación de Rollup del apartado 2 se aplica específicamente al escenario de publicar una librería de componentes reutilizable por otros proyectos, no al de desplegar una aplicación completa como TaskFlow. TaskFlow no tiene ningún consumidor externo que vaya a volver a empaquetar su código con su propio bundler; su objetivo es generar directamente el resultado final que se sirve a los usuarios, con toda la experiencia de desarrollo (recarga en caliente durante el curso) y la configuración de producción orientada a aplicaciones (división en fragmentos, gestión de caché por hash de fichero) que Vite ya ofrece de fábrica, y que además usa Rollup por debajo para su propio build de producción. Cambiar a Rollup de forma directa para TaskFlow no aportaría ninguna ventaja sobre vite build y solo obligaría a reconstruir manualmente buena parte de esa configuración de aplicación que Vite ya resuelve por defecto.

Conclusión

Con esta lección se cierra el módulo 8: TaskFlow puede ya integrarse en HTML plano, dentro de otros frameworks, renderizarse en el servidor, y empaquetarse tanto como librería reutilizable de componentes como aplicación desplegable, con la opción adicional —puramente sintáctica, sin cambiar ningún concepto ya estudiado— de escribirse en TypeScript con decoradores en lugar de static properties. Con TaskFlow ya integrable y desplegable, falta darle cobertura de pruebas y repasar buenas prácticas antes del proyecto final.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados