Enlace al commit en Github de este capítulo, se abrirá en una nueva pestaña

Estructura del paquete

Vamos a crear un componente de ejemplo llamado hero. La estructura típica del paquete será:

packages/
└── hero/
    ├── src/
    │   ├── Hero.tsx
    │   ├── index.ts
    │   └── styles.css
    ├── dist/              # (generado tras build)
    ├── package.json
    ├── tsconfig.json
    ├── tsconfig.build.json
    └── .swcrc

package.json del paquete

Cada paquete es una unidad autocontenida. Aquí tienes un ejemplo:

{
    "name": "@myui/hero",
    "version": "0.1.0",
    "main": "dist/index.js",
    "module": "dist/index.js",
    "types": "dist/index.d.ts",
    "type": "module",
    "files": ["dist"],
    "sideEffects": false,
    "exports": {
        ".": {
            "import": "./dist/index.js",
            "types": "./dist/index.d.ts",
            "default": "./dist/index.js"
        },
        "./css": {
            "import": "./dist/styles.css",
            "require": "./dist/styles.css",
            "default": "./dist/styles.css"
        },
        "./module.css": {
            "import": "./dist/styles.module.css",
            "require": "./dist/styles.module.css",
            "default": "./dist/styles.module.css"
        }
    },
    "scripts": {
        "build": "pnpm clean && pnpm build:swc && pnpm build:types && pnpm copyfiles && pnpm build:moduleCss",
        "build:swc": "swc ./src -d ./dist --config-file .swcrc --ignore **/*.test.tsx,**/*.test.ts,**/*.stories.tsx --strip-leading-paths",
        "build:types": "tsc --emitDeclarationOnly --project tsconfig.build.json --outDir dist",
        "build:moduleCss": "sed -e 's/.*__/./' -e '/:root {/,/}/d' src/styles.css > dist/styles.module.css",
        "clean": "rm -rf dist",
        "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
        "lint": "eslint src",
        "lint:fix": "eslint --fix --ext .ts,.tsx src",
        "test": "jest",
        "prepublishOnly": "pnpm clean && pnpm build"
    },
    "dependencies": {
        "react": "19.1.0"
    },
    "devDependencies": {
        "@types/react": "^19.1.8",
        "@types/react-dom": "^19.1.6",
        "copyfiles": "^2.4.1",
        "typescript": "5.8.3"
    }
}

Configuración de TypeScript

tsconfig.json del paquete:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

tsconfig.build.json (solo para tipos):

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "emitDeclarationOnly": true,
    "declaration": true,
    "declarationMap": true,
    "noEmit": false
  }
}

Compilación con SWC

Usamos swc para compilar el código de TypeScript/JSX a JS moderno, sin el coste de Babel.

.swcrc en cada paquete:

{
  "$schema": "https://swc.rs/schema.json",
  "sourceMaps": true,
  "jsc": {
    "target": "esnext",
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "dts": true
    },
    "transform": {
      "react": {
        "runtime": "automatic",
        "pragmaFrag": "React.Fragment",
        "throwIfNamespace": true,
        "development": false,
        "useBuiltins": true
      }
    }
  },
  "module": {
    "type": "es6"
  }
}

CSS modular exportable

Convertimos styles.css a styles.module.css en el dist/ automáticamente, gracias al uso de la metodología BEM (Block Element Modifier), se abrirá en una nueva pestaña y el siguiente script que tenemos en nuestro package.json:

sed -e 's/.*__/./' -e '/:root {/,/}/d' src/styles.css > dist/styles.module.css

Esto permite exponer estilos reutilizables sin exponer variables globales.

Creemos nuestro primer componente

Al ser un componente de ejemplo, vamos a crear un componente de hero simple, con pocas variables y estilos básicos.

Creamos el archivo src/Hero.tsx:

import React from "react";

type HeroProps = React.PropsWithChildren<{
  title: string;
  description?: string;
  image?: string;
}> & React.HTMLAttributes<HTMLElement>;

export function Hero({
  className,
  title,
  description,
  image,
  children,
  ...props
}: HeroProps) {
  const backgroundImage = image ? { backgroundImage: `url(${image});` } : {};
  return (
    <section
      className={`hero ${className}`}
      {...props}
      style={{ ...(props?.style ?? {}), ...backgroundImage }}
    >
      <div className="hero__content">
        <h1 className="hero__title">{title}</h1>
        {description && <p className="hero__description">{description}</p>}
        {children}
      </div>
    </section>
  );
}

Recibirá el título, la descripción y la imagen de fondo, y renderizará un section con el contenido.

Creamos el archivo src/index.ts:

export * from "./Hero";

Creamos el archivo src/styles.css:

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100dvh;
  width: 100%;
  background-size: cover;
  background-position: center;
}

.hero__content {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  width: 100%;
  max-width: 60rem;
  padding: 1rem 1.5rem;
}

.hero__title {
  font-size: 2.5rem;
  font-weight: 700;
}

Como mencionamos anteriormente, usaremos la metodología BEM (Block Element Modifier), se abrirá en una nueva pestaña para nombrar crear el archivo module automaticamente.

Prueba rápida

Ahora puedes hacer:

# Instalamos las dependencias de todos los paquetes
pnpm install

# Compilamos el paquete recien creado
pnpm build --filter @myui/hero

Y verás tu código transpilado y tipado dentro de dist/.

Finalmente, añadimos las siguientes carpetas a nuestro .gitignore para que no se incluya en el commit:

.turbo
dist

Próximo capítulo

En el Capítulo 3:

  • Añadiremos jest o vitest para pruebas unitarias.
  • Configuraremos @testing-library/react, jest-axe y jsdom.
  • Integraremos testing accesible desde el primer componente.
Ir al siguiente capítulo

Volver a la lista de capítulos