# Extensiones de compilación

Elide integra un compilador, un resolvedor de Maven, un ensamblador de contenedores y un ejecutor de pruebas. No integra `protoc`, `shellcheck` ni lo que su proyecto necesite a continuación. Una **extensión de compilación** es la forma de enseñarle a su compilación una herramienta más: un módulo JavaScript o TypeScript que Elide evalúa mientras configura la compilación.

Una extensión declara tareas. Cada una se convierte en una tarea real del grafo: puede nombrarla como destino de compilación, hacer que otra tarea dependa de ella, ponerla en un grupo y verla en `elide build --inspect`.

## Declarar una extensión

Indíquela en su manifiesto, relativa a la raíz del proyecto:

```pkl
amends "elide:project.pkl"

name = "demo"

extensions {
  "tools/protoc.ts"
}
```

Las extensiones se declaran, nunca se descubren, así que ejecutar una siempre es algo que su proyecto pidió. Una ruta que no existe hace fallar la compilación en lugar de no registrar nada en silencio.

Una ruta se resuelve contra la raíz del proyecto y debe quedarse dentro de su árbol de fuentes: la raíz del proyecto, o la raíz del espacio de trabajo cuando el proyecto es miembro de uno, para que los miembros puedan compartir un archivo situado por encima de sus propias raíces (`../tools/protoc.ts`). Una ruta absoluta fuera del árbol, una ruta que sale de él y un enlace simbólico que apunta fuera de él se rechazan, y cada uno se notifica como cualquier otra declaración incorrecta.

Una declaración nombra una de dos cosas, y Elide decide cuál solo por la extensión del archivo: un módulo ES (`.ts`, `.mts`, `.js`, `.mjs`, `.jsx`, `.tsx`) o un archivo de paquete `.elx` (consulte [Empaquetar una extensión](#packaging-an-extension)). `.cjs` no se acepta: el contrato es una exportación predeterminada, que un script CommonJS no puede declarar, y un `.js` siempre se lee como módulo diga lo que diga el `package.json` más cercano. Cualquier otra cosa hace fallar la compilación en lugar de intentar adivinarla.

Una entrada no tiene por qué ser una ruta: puede nombrar un paquete que se descarga de una URL, de npm o de Maven. Consulte [Descargar una extensión](#fetching-an-extension).

## Escribir una

```ts
export default ({ task }: ElideBuild) => {
  task(
    "protoc",
    {
      description: "Generate protobuf sources",
      group: "compile",
      inputs: ["proto/**/*.proto"],
      outputs: ["src/main/generated"],
    },
    (ctx) => {
      const result = ctx.exec("protoc", [
        `--kotlin_out=${ctx.layout.projectDir}/src/main/generated`,
        "proto/api.proto",
      ]);
      if (!result.ok) ctx.fail(result.stderr);
    },
  );
};
```

Después:

```bash
elide build --inspect   # protoc  Generate protobuf sources
elide build protoc      # run just that task
elide build compile     # run it with everything else in its group
```

Los tipos se publican en [`@elide-dev/types`](https://www.npmjs.com/package/@elide-dev/types) como `ElideBuild`, `ElideBuildTaskContext` y `ElideBuildTaskOptions`.

La exportación predeterminada de su módulo se ejecuta mientras se configura la compilación y puede ser `async`. La acción de cada tarea se ejecuta cuando lo hace la tarea y también puede ser `async`; la compilación espera en ambos casos.

## Empaquetar una extensión

Una extensión que ya no cabe en un solo archivo se distribuye como archivo comprimido: un zip con un manifiesto llamado `extension.json` en su raíz y con nombre `.elx`. El nombre no es decorativo: la extensión de archivo de la declaración es lo que indica a Elide que se trata de un paquete, y `.elx` es la única que lo hace, así que `tools/protoc.elx` se carga y los mismos bytes con el nombre `tools/protoc.zip` no.

```
protoc-tasks/
├── extension.json
├── tasks.ts
└── lib/
    └── flags.ts
```

```json
{
  "name": "protoc-tasks",
  "version": "1.0.0",
  "description": "Generates sources with protoc",
  "license": "Apache-2.0",
  "repository": "https://github.com/example/protoc-tasks",
  "homepage": "https://example.dev/protoc-tasks",
  "authors": ["Example Inc."],
  "type": "source",
  "entrypoint": "tasks.ts"
}
```

El manifiesto del archivo exige `name` y `type`. `type` es `source` (el único tipo que existe), y un paquete `source` también debe indicar un `entrypoint`, una ruta relativa a la raíz del archivo; se rechaza uno que apunte fuera del archivo. Todo lo demás son metadatos que Elide le devuelve, y las claves que no conoce se ignoran, así que un paquete puede llevar claves propias.

### Compilar el paquete

Declare el paquete como artefacto y `elide build` lo produce:

```pkl
amends "elide:project.pkl"

import "elide:Extensions.pkl" as Extensions

name = "protoc-tasks"
version = "1.0.0"
description = "Generates sources with protoc"

artifacts {
  ["tasks"] = new Extensions.ExtensionArchive {
    entrypoint = "tasks.ts"
    srcs { "lib/**/*.ts" }
    license = "Apache-2.0"
    repository = "https://github.com/example/protoc-tasks"
    authors { "Example Inc." }
  }
}
```

Eso registra una tarea llamada `package-extension-tasks`, que ejecuta un simple `elide build` y que escribe `.dev/artifacts/tasks.elx`. El `extension.json` anterior se escribe automáticamente: `name`, `version` y `description` toman por defecto los del proyecto, y `type` siempre es `source`.

`entrypoint` es el módulo cuya exportación predeterminada declara sus tareas. `srcs` son los módulos que importa, como rutas o patrones glob relativos a la raíz del proyecto; el punto de entrada siempre se empaqueta. Los archivos conservan las rutas que tienen en su proyecto, de modo que las importaciones relativas se resuelven dentro del archivo exactamente igual que en su copia de trabajo. Cada archivo empaquetado debe estar dentro del proyecto una vez resueltos sus enlaces: un enlace simbólico que apunta fuera del proyecto hace fallar la compilación en lugar de distribuir aquello a lo que apunta.

Por supuesto, también puede crear el zip usted mismo, desde el directorio que contiene el manifiesto, para que `extension.json` quede en la raíz:

```bash
cd protoc-tasks && zip -r ../tools/protoc.elx .
```

### Un paquete de fuentes

`"type": "source"` significa que el archivo lleva su código fuente, y la extensión de archivo del punto de entrada decide cómo se carga, igual que con un archivo que declara directamente. Un punto de entrada `.ts`, `.tsx`, `.mts`, `.js`, `.jsx` o `.mjs` se evalúa como módulo invitado y, como Elide descomprime el archivo antes de evaluarlo, las importaciones relativas dentro del paquete (`import { flags } from "./lib/flags.ts"`) se resuelven.

Nada se empaqueta ni se compila por el camino: un paquete distribuye las fuentes que usted escribió, y la extensión que escribe es la misma tanto si viaja como un archivo como si lo hace como archivo comprimido: la misma exportación predeterminada, el mismo `task(...)`, el mismo `ctx`.

### Declarar un paquete

Como una extensión local (una ruta, con ajustes si los admite) o como algo que se descarga:

```pkl
extensions {
  "tools/shellcheck.elx"
  new {
    from = "tools/protoc.elx"
    config { greeting = "hei" }
  }
}
```

Elide descomprime el archivo en su directorio de compilación, en `.dev/extensions/packages/<archive>-<hash>/contents/`, y vuelve a descomprimirlo cada vez que el archivo cambia. Una entrada que se escribiría fuera de ese directorio se rechaza en lugar de descomprimirse.

### Qué muestra `--inspect`

`elide build --inspect` imprime una sección `Extensions:` que cubre todo lo que declaró su manifiesto. Para cada una muestra la declaración que escribió, el nombre y la versión del paquete, su descripción, qué tipo de extensión es (`JavaScript/TypeScript module` o `packaged extension`), su licencia, su repositorio y los nombres de las tareas que declaró.

```
Extensions:
  tools/protoc.elx   protoc-tasks 1.0.0
    Generates sources with protoc
    packaged extension · Apache-2.0 · https://github.com/example/protoc-tasks · Example Inc.
    tasks: protoc-generate, protoc-clean
```

La distribución del archivo corre de su cuenta: adjúntelo a una versión, sírvalo desde una URL o publíquelo como paquete npm con lo que ya utilice. Viaje como viaje, un proyecto lo consume de la misma forma; consulte [Descargar una extensión](#fetching-an-extension).

## Descargar una extensión

Una extensión no tiene por qué estar en su proyecto. Una declaración que empieza por uno de tres esquemas reservados nombra algo que se descarga:

```pkl
extensions {
  "url:https://downloads.example.com/protoc-tasks-1.4.0.elx"
  "npm:@acme/protoc-tasks@1.4.0"
  "maven:com.acme:protoc-tasks:elx:1.4.0"
  new {
    from = "npm:@acme/codegen@2.0.1"
    config { proto = "src/proto" }
  }
}
```

Solo `url:`, `npm:` y `maven:` están reservados. Todo lo demás es una ruta, así que `C:\tools\ext.ts` sigue significando lo que dice, y un esquema que Elide añada más adelante no puede cambiar cómo se lee un manifiesto que ya compila.

Pasada la descarga, nada cambia. Un `.elx` descargado se carga como paquete y un `.ts` descargado como módulo invitado: decide aquello en lo que se resolvió, no cómo llegó. Una `url:` debe ser una URL `https` absoluta; el `http` sin cifrar se rechaza salvo hacia un host de loopback, que existe para que pueda probar contra algo local. Las redirecciones se siguen (hasta cinco saltos) y cada salto debe cumplir la misma regla, así que una redirección no puede llevar una descarga de loopback a otra máquina sin cifrar.

### Las versiones están fijadas

Una versión remota nombra exactamente una publicación. El esquema del manifiesto rechaza `npm:tasks` y `maven:com.acme:tasks` por no nombrar ninguna, y `npm:tasks@^1.2`, `npm:tasks@latest` y `maven:com.acme:tasks:1.2.3-SNAPSHOT` por nombrar más de una.

Es cosa de la caché. Una extensión descargada se guarda bajo lo que usted declaró, así que una versión cambiante o bien costaría una consulta al registro en cada compilación (lo que haría imposible `--offline`), o bien se congelaría en silencio en lo que viera su primera compilación.

### Dónde se almacena en caché

Los artefactos descargados se guardan en `.dev/dependencies/extensions/remote/`, junto a su repositorio Maven resuelto y compartidos por todo un espacio de trabajo de la misma forma. Como la declaración está fijada, lo que hay en caché nunca está obsoleto: solo la primera compilación accede a la red, y `elide build --offline` funciona a partir de entonces. Una declaración que no se ha descargado y no puede descargarse lo indica:

```
Build extension 'npm:@acme/protoc-tasks@1.4.0' has not been downloaded, and --offline forbids fetching it
Run the build once without --offline to populate the cache.
```

Se comprueba cualquier resumen criptográfico que publique el origen: `dist.integrity` o `dist.shasum` de npm, y el archivo `.sha1` adjunto de Maven. Un repositorio que sirve un artefacto sin resumen es válido; uno que sirve ambos y se contradice hace fallar la compilación.

### Fijar los bytes

Una versión fijada indica qué publicación se ejecuta; un `integrity` fijado indica qué bytes. Escríbalo como Subresource Integrity (`sha256-<base64>` o `sha512-<base64>`) junto al origen al que pertenece:

```pkl
extensions {
  new {
    from = "url:https://downloads.example.com/protoc-tasks-1.4.0.elx"
    integrity = "sha512-Ln7jRoUMYMK7TBZ0bNyPmrHDCgAsQ3bIt5nZeUuzpuGqBbw1zYWTPdA2tRbClC2CoRP3UExpHnCw+5kZLgP0lQ=="
  }
}
```

Una `url:` solo se valida por TLS y por su host, así que el valor fijado es lo que la hace reproducible. Se comprueba antes de guardar la descarga en caché y de nuevo en cada acierto de caché, y un paquete npm se ejecuta desde un árbol extraído de esos mismos bytes verificados, así que ni el servidor ni la caché compartida del espacio de trabajo pueden cambiar lo que se ejecuta a sus espaldas. Un archivo en caché que ya no coincide con el valor fijado se vuelve a descargar y se reemplaza; con `--offline`, la compilación falla e indica el archivo en caché. El valor fijado se suma a lo que publique el registro en lugar de reemplazarlo: el resumen de npm o de Maven se sigue comprobando.

Solo un origen descargado lo admite. Fijar una ruta dentro de su proyecto se rechaza: sus bytes ya son los que contenga su árbol.

Una descarga está limitada en tamaño y en tiempo: falla una respuesta de más de 64 MiB, y también un host que responde y luego se detiene, en lugar de bloquear su configuración.

### Paquetes npm

El tarball se guarda en caché una vez para el espacio de trabajo y se extrae en el directorio de compilación de su proyecto, en `.dev/extensions/npm/<name>/<version>/contents/`, y se carga desde su punto de entrada: el que indique `extension.json` si el paquete lo incluye; si no, el `main` de `package.json`; si no, `index.js`. Un paquete que lleva un `extension.json` es un paquete en sentido pleno y se lee exactamente igual que un `.elx`: uno que no es válido, o que nombra un `type` que esta versión de Elide no conoce, hace fallar la compilación en lugar de recurrir a `main`.

La resolución sigue tres pasos, en orden: `dependencies.npm.registry` cuando su manifiesto lo define; después `$ELIDE_PKGST_NPM_ENDPOINT` (la misma variable que lee el resolvedor npm de Elide), y si ambos faltan, el proxy `npm.pkg.st` de Elide, o el registro público en `registry.npmjs.org` cuando pasa `--use-pkgst=false`.

```pkl
dependencies {
  npm {
    registry = "https://npm.internal.example.com"
  }
}
```

### Artefactos de Maven

La coordenada se descarga de los repositorios que ya declara (`dependencies.maven.repositories`, más el proxy de Elide y Maven Central salvo que `enableDefaultRepositories` esté desactivado) con las credenciales que llevan esos repositorios. Las credenciales solo se envían al esquema, host y puerto del propio repositorio; una redirección a cualquier otro lugar se sigue sin ellas. El habitual `[:packaging[:classifier]]` va entre el artefacto y la versión, y una coordenada sin empaquetado toma `elx`: `maven:com.acme:tasks:1.4.0` descarga `tasks-1.4.0.elx`, igual que `maven:com.acme:tasks:elx:1.4.0`. Un empaquetado que Elide no puede cargar (`maven:com.acme:tasks:zip:1.4.0`) se rechaza como cualquier declaración que no se puede cargar.

Los repositorios se prueban en orden. Uno que responde con un error, con una respuesta demasiado grande o que se detiene, o con una redirección rechazada, se descarta en favor del siguiente; un `.sha1` publicado que no coincide con el artefacto hace fallar la compilación.

Se descarga un artefacto y no se resuelve ningún grafo de dependencias, exactamente igual que con un archivo declarado por ruta. Una extensión que necesita bibliotecas las incluye dentro de su paquete.

## Configurar una extensión

Una extensión que admite ajustes se declara como un bloque en lugar de una ruta simple, y los lleva consigo. Elide no los interpreta:

```pkl
extensions {
  "tools/shellcheck.ts"
  new {
    from = "tools/protoc.ts"
    config {
      version = "25.1"
      plugins = new Listing { "grpc" }
    }
  }
}
```

Ese bloque llega a esa extensión, y solo a ella, como `config`, tanto mientras declara tareas como mientras estas se ejecutan:

```ts
export default ({ task, config }: ElideBuild) => {
  task("protoc", { description: `protoc ${config.version}` }, (ctx) => {
    const { version, plugins } = ctx.config;
  });
};
```

Una extensión declarada con una ruta simple lee un `config` vacío, así que basta con comprobar si un ajuste es `undefined` para aplicar un valor predeterminado.

Una extensión que quiere ajustes tipados y documentados distribuye un módulo Pkl propio, que su proyecto copia, importa y rellena. Un manifiesto importa archivos locales, la biblioteca estándar de Pkl y el esquema propio de Elide, nunca un `package://` ni otra URI de red, así que el módulo vive en su proyecto:

```pkl
import "tools/Protoc.pkl"

extensions {
  new {
    from = "tools/protoc.ts"
    config = new Protoc.Settings { version = "25.1" }
  }
}
```

Las entradas se distinguen por ruta, se escriban como se escriban, así que la misma extensión no puede declararse (ni configurarse) dos veces.

Escriba una lista dentro de un bloque como `Listing` (o `List("grpc")`). Los elementos en línea, como `plugins { "grpc" }`, llegarían a su extensión indexados por posición en lugar de como lista, así que el manifiesto los rechaza y le indica la forma que funciona.

Los ajustes son datos. Un valor puede ser una cadena, un booleano, un número, un `Duration`, un `DataSize` (que llega a su extensión como número de bytes) o un `Regex`, o un bloque, `Listing`, `List`, `Set`, `Map` o `Pair` de ellos. Un `Bytes`, una clase, un alias de tipo o una función es un error del manifiesto que indica dónde se encuentra, como `config.flags.blob`.

## Opciones de tarea

| Opción | Significado |
| --- | --- |
| `description` | Lo muestra `elide build --inspect`. |
| `dependsOn` | Nombres de tareas que deben completarse antes; un solo nombre puede indicarse sin array. |
| `before` | Tareas a las que esta debe preceder: un nombre de tarea, o `deps`/`compile`/`test` para todas las tareas de ese grupo. |
| `group` | `deps`, `compile` o `test` para unirse a un grupo integrado; cualquier otro nombre crea uno propio. |
| `default` | Si un `elide build` sin argumentos ejecuta esta tarea. Por defecto, `true`. |
| `inputs` | Rutas o globs relativos al proyecto cuyo contenido decide si la tarea se vuelve a ejecutar. |
| `outputs` | Rutas o globs relativos al proyecto que escribe la tarea. |
| `cache` | Si la caché de compilación puede archivar las salidas de esta tarea. Por defecto, `true`. |
| `options` | Opciones de línea de comandos que acepta esta tarea (vea más abajo). |

`inputs` y `outputs` son un único contrato: declare ambos y Elide omite la tarea cuando nada ha cambiado (consulte [Tareas de compilación](/docs/toolchain/build-tasks)); declare uno o ninguno y se ejecuta en cada compilación. Su extensión también cuenta como entrada (el propio archivo y cada módulo que importa por ruta relativa), así que editar el punto de entrada o uno de sus auxiliares vuelve a ejecutar las tareas que declara.

Una tarea que declaró ambos también se almacena en caché como las de Elide: sus salidas se archivan cuando tiene éxito y se restauran en cualquier compilación posterior cuyas entradas tengan la misma huella, también en una copia de trabajo nueva. Defina `cache: false` para conservar la comprobación de actualización y omitir el archivado; es lo adecuado cuando sus salidas dependen de algo que sus `inputs` no nombran.

La huella de una declaración se calcula a partir del modo y el contenido de cada archivo que nombra, y su acción lee los mismos archivos y las mismas huellas mediante `ctx.inputs` y `ctx.outputs`; consulte [Los archivos que nombra una declaración](#the-files-a-declaration-names).

## Opciones que acepta su tarea

Una tarea que lee una opción debe declararlo. Las opciones declaradas las imprimen `elide build --inspect` y `elide build <task> --help`, y se comprueban antes de ejecutar nada, así que un error tipográfico se notifica en lugar de descartarse en silencio:

```ts
task(
  "publish",
  {
    options: [
      { name: "channel", description: "Channel to publish to", arity: "required" },
      { name: "tag", description: "Extra tag to push", arity: "required", repeatable: true },
      { name: "sign", description: "Sign the release", negatable: true, default: "true" },
    ],
  },
  (ctx) => {
    ctx.option("channel"); // first value, or the declared default
    ctx.values("tag"); // every value, in command-line order
    ctx.flag("sign"); // `--no-sign` reads false; absent reads the default
  },
);
```

| Campo | Significado |
| --- | --- |
| `name` | La opción, sin guiones y en kebab-case: `"dry-run"` es `--dry-run`. |
| `description` | Lo muestran `--inspect` y `--help`. |
| `arity` | `"none"` es una opción sin valor, `"required"` exige `--flag=value`, `"optional"` admite ambas formas. Por defecto, `"none"`. |
| `repeatable` | Si la opción puede pasarse más de una vez. Por defecto, `false`. |
| `negatable` | Si se acepta `--no-<name>`, que se lee como `"false"`. Por defecto, `false`. |
| `default` | El valor que se lee cuando la opción no se pasó. |
| `keyed` | Si lo que se pasó a la opción decide lo que produce la tarea. Por defecto, `true`. |

`--inspect` y `--help` imprimen la declaración tal como hay que escribirla, así que las tres anteriores se leen `--channel=<value>`, `--tag=<value> (repeatable)` y `--sign (or --no-sign) [default: true]`.

Una opción que nada declara hace fallar la compilación, indicando el nombre declarado más parecido si lo hay; lo mismo ocurre con un valor dado a una opción sin valor, una opción obligatoria sin valor y la repetición de una opción que admite un solo valor. Las opciones propias de Elide (`--verbose`, `--project`, `--offline` y las demás) siempre se aceptan.

`ctx.option`, `ctx.flag` y `ctx.values` leen a través de la declaración, que es lo que permite leer `--no-sign` y un valor predeterminado: ninguno de los dos es algo que la línea de comandos diga por sí sola. Un nombre que su tarea no declaró se sigue leyendo de la línea de comandos tal como se analizó.

Una opción declarada forma parte de lo que decide si su tarea se vuelve a ejecutar: los valores a los que se resolvió se registran junto a sus `inputs`, así que una compilación con `--channel=stable` nunca se considera actualizada (ni recibe lo que `--channel=canary` guardó en caché) respecto a una que se ejecutó con la otra. Defina `keyed: false` en una opción que cambia cuánto informa la tarea y no lo que escribe; si no, cada compilación que la escriba de otra forma fallará en la caché sin motivo. Una opción que su acción lee sin declararla no decide nada.

## Ejecutar antes de otra tarea

`dependsOn` nombra lo que debe ejecutarse antes. `before` nombra lo que debe esperar, incluidas tareas que usted no declaró, que es lo que necesita una tarea de generación de código:

```ts
task(
  "generate-sources",
  { before: "compile", default: false, outputs: ["src/main/generated"] },
  (ctx) => { ... },
);
```

`compile`, `deps` y `test` representan todas las tareas de ese grupo, que es lo que debe significar «antes de la compilación»: las claves reales son por lenguaje y por conjunto de fuentes (`compile-kotlin-main`, `compile-java-test`). Cualquier otro nombre es el de una sola tarea, y un nombre que nada registró hace fallar la compilación en lugar de no ejecutar nada en silencio. En un espacio de trabajo, una extensión que declara un miembro selecciona solo las tareas de ese miembro, mientras que una que declara el manifiesto raíz selecciona el grupo en todos los miembros.

Una arista así también incorpora su tarea a cualquier compilación que ejecute la tarea a la que precede: `elide build compile` ejecuta antes `generate-sources` en el ejemplo anterior, aunque esta haya renunciado a `default`.

## Compilaciones incrementales

Una tarea en ejecución recibe lo que la compilación encontró obsoleto y puede conservar valores para su siguiente ejecución:

```ts
task("codegen", { inputs: ["proto", "schema"], outputs: ["gen"] }, (ctx) => {
  for (const input of ctx.dirty.inputs) ctx.log(`changed: ${input}`);
  if (ctx.dirty.outputs.length) ctx.log("an output is missing or was edited");
  if (ctx.dirty.extension) ctx.log("this extension or one of its imports changed");

  ctx.log(`last built version ${ctx.state.get("version") ?? "(never)"}`);
  ctx.state.set("version", ctx.manifest.version);
});
```

`ctx.dirty` nombra solo las declaraciones que cambiaron; en una primera ejecución se enumera todo lo que declaró. `ctx.state` guarda cadenas, números, booleanos, `null`, arrays y objetos simples, indexados por nombre, y `get` devuelve `null` para una clave en la que no se guardó nada. Una función, un símbolo y `undefined` no tienen forma almacenada, y un valor que se contiene a sí mismo se rechaza en lugar de recorrerse. Una ejecución que falla descarta por completo el estado de la tarea, incluido lo que registraron ejecuciones anteriores.

Lo que una tarea recuerda es una nota, no una entrada: nunca decide si la tarea se vuelve a ejecutar y nunca cambia la clave de caché de la tarea.

## Los archivos que nombra una declaración

`ctx.inputs` y `ctx.outputs` son las propias declaraciones, resueltas y con su huella: el mismo recorrido y el mismo resumen que calculó la compilación para decidir si su tarea estaba actualizada:

```ts
task("codegen", { inputs: ["proto"], outputs: ["gen"] }, (ctx) => {
  for (const file of ctx.inputs.files()) {
    ctx.log(`${file.path} ${file.size}b ${file.stamp}`);
  }

  const before = ctx.state.get("proto") ?? {};
  const after = Object.fromEntries(ctx.inputs.files().map((f) => [f.path, f.stamp]));
  for (const path of Object.keys(after)) {
    if (after[path] !== before[path]) ctx.exec("protoc", ["--kotlin_out=gen", path]);
  }
  ctx.state.set("proto", after);

  ctx.log(`produced ${ctx.outputs.stamp()}`);
});
```

`files()` enumera cada archivo normal que nombran las declaraciones, relativo al proyecto y separado por `/`, ordenado y sin duplicados. Cada entrada lleva `path`, `absolute`, `size`, `modified` y `stamp`, un SHA-256 del modo y el contenido de ese archivo. `stamp()` es el resumen de todo el conjunto.

Ambos admiten sus propios patrones, que no tienen por qué ser los que declaró: `ctx.inputs.files("proto/api/**/*.proto")` acota un listado, y `ctx.inputs.stamp("proto")` es exactamente la huella que la compilación registró para esa declaración. Ambos se resuelven al llamarlos, así que leer `ctx.outputs` después de escribir sus salidas describe lo que acaba de producir.

Las huellas dependen del contenido: una edición que no cambia el tamaño ni la fecha de modificación de un archivo sí las cambia, y vuelve a ejecutar su tarea. `ctx.dirty` nombra una declaración completa; una huella por archivo guardada en `ctx.state`, como en el ejemplo, es la forma en que una tarea sabe cuáles de sus archivos cambiaron.

## Declaraciones que la compilación debe calcular

Parte de lo que decide si una tarea está al día no puede escribirse en la declaración: un árbol generado, un classpath que resolvió otra tarea, la versión de la herramienta que está a punto de ejecutar. Dé a la tarea un hook `prepare` y declárelo ahí:

```ts
task(
  "package",
  {
    outputs: ["dist"],
    prepare: (ctx) => {
      ctx.inputs.add("gen/**/*.kt");
      ctx.inputs.value("classpath", ctx.state.of("resolve")?.get("classpath"));
      ctx.outputs.add(`dist/${ctx.manifest.version}.tgz`);
    },
  },
  (ctx) => {
    for (const changed of ctx.dirty.values) ctx.log(`${changed} changed`);
  },
);
```

Lo que añade un hook es una declaración como cualquier otra: un patrón recibe su huella, aparece en `ctx.dirty.inputs` y entra en la clave de caché de la tarea exactamente igual que uno declarado. `ctx.inputs.value(name, value)` declara una entrada que no es un archivo (cualquier cosa que la compilación puede calcular pero no consultar con stat), y un cambio en ella vuelve a ejecutar la tarea y cambia la clave de su archivo; `ctx.dirty.values` nombra cuál cambió. Una tarea cuya declaración se completa así se rastrea, se almacena en caché y se omite como actualizada como cualquier otra.

El hook se ejecuta en cada compilación en la que participa su tarea, incluidas aquellas en las que resulta no tener nada que hacer, así que limítelo a derivar declaraciones: no tiene `exec`, ni `tools`, ni `dirty` (esa es la respuesta que ayuda a calcular), y `ctx.state` es de solo lectura ahí. Devolver `false` hace fallar la tarea antes de que se ejecute.

Su acción puede añadir **salidas** que solo conoce al escribirlas, con el mismo `ctx.outputs.add(...)`; reciben su huella y se archivan con las demás cuando retorna. No puede añadir entradas: para entonces la compilación ya decidió si ejecutarla, así que un `ctx.inputs.add` tardío es un error en lugar de una declaración que no hace nada en silencio.

## Pasar un valor a otra tarea

El módulo de su extensión se evalúa una vez por compilación y lo comparten todas las tareas que declara, así que una variable de módulo que asigna una tarea a menudo sigue ahí para la siguiente, pero nada lo garantiza, y una evaluación nueva empieza de cero. Trate el ámbito del módulo como una caché, nunca como un canal. Lo que una tarea guarda en `ctx.state` es la forma admitida de pasar un valor:

```ts
task("stage", (ctx) => {
  ctx.state.set("staged", `${ctx.layout.buildDir}/staged`);
});

task("publish", { dependsOn: "stage" }, (ctx) => {
  const stage = ctx.state.of("stage");
  if (stage === null) return ctx.skip("nothing has been staged");
  ctx.exec("rsync", [String(stage.get("staged")), "example:/srv"]);
});
```

`ctx.state.of(name)` devuelve una vista de lo que tiene esa tarea (`get`, `has` y `keys`) y `null` cuando no publicó nada. Una tarea que la compilación omitió por estar actualizada, o que restauró de la caché, responde con lo que guardó la última vez que se ejecutó: estar actualizada es una afirmación sobre lo que produjo, y lo que publicó forma parte de ello. `null` significa que no había nada que leer: el nombre no es una tarea declarada por una extensión, no estaba en esta compilación, falló o aún no se ha ejecutado; una vista sin claves es una tarea que no guardó nada. Haga que su tarea dependa de la que lee; sin una arista, «aún no se ha ejecutado» es una condición de carrera.

Un valor pasa con la estructura que tenía, no como texto, y a lo que puede pasar se le aplican las mismas reglas que a `ctx.state`. Leer su propio nombre devuelve lo que tiene en ese momento.

## Lo que recibe su tarea

El objeto que se pasa a su acción lleva lo mismo que lee la propia compilación:

- `project`: nombre, versión, descripción, directorio raíz y el espacio de trabajo al que pertenece.
- `manifest`: su `elide.pkl` resuelto, como datos JavaScript normales. Es exactamente lo que escribe `elide project export --format=json`, así que `manifest.dependencies.maven.packages.map(...)` funciona como cabría esperar.
- `config`: los ajustes que su manifiesto declaró para esta extensión (vea más arriba), vacío si no declaró ninguno.
- `layout`: `projectDir`, `buildDir`, `artifacts`, `staging` y similares, como rutas absolutas.
- `option(name)` / `flag(name)` / `values(name)`: las opciones con las que se ejecutó esta invocación, leídas a través de su declaración (vea más arriba); `options` es el análisis sin procesar, para lo que no declaró.
- `task`: su propia declaración, tal como la registró la compilación, incluidas las opciones que declara.
- `dirty` / `state`: lo que cambió desde la última ejecución de su tarea, lo que guardó y lo que registraron otras tareas de esta compilación (vea más arriba).

## Informar del resultado

| En su acción | Lo que registra la compilación |
| --- | --- |
| retornar, o retornar `true` | éxito |
| `ctx.fail(reason)` | fallo, con ese motivo |
| retornar `false` | fallo, sin motivo |
| `ctx.skip(reason)` | omitida, con ese motivo |
| lanzar una excepción o rechazar | fallo, con su mensaje |
| `ctx.exit(code, reason)` | fallo que termina con `code` |

`ctx.log`, `ctx.warn` y `ctx.error` escriben líneas atribuidas a su tarea. Son mensajes de compilación, así que van a la salida de error estándar junto con todos los demás mensajes de compilación; la salida estándar solo lleva lo que escribe un programa que usted ejecutó.

### Terminar con un estado concreto

`ctx.exit(code, …message)` hace fallar la tarea y pide a la compilación que termine con `code`. `0` indica éxito, y eso es lo que convierte esta llamada en toda la decisión de éxito o fallo para una tarea que envuelve un programa:

```ts
ctx.exit(ctx.exec("shellcheck", ["scripts/build.sh"]).exitCode);
```

Úselo cuando un estado tiene un significado que un booleano destruiría. Un trinquete de corrección que termina con 2 porque el analizador falló y con 1 porque encontró una nueva infracción solo es útil si quien lo llama puede distinguirlos; lo mismo ocurre con un diagnóstico que termina con 2 para «no compatible con este host» y con 1 para «encontró algo».

El código llega al proceso solo cuando su tarea es la que detuvo la compilación; si otra tarea falla antes, gana esa. Un código fuera de `0`-`255` se ajusta como lo notifica una shell, y nunca se ajusta a cero.

## Ejecutar herramientas externas

```ts
const result = ctx.exec("shellcheck", ["scripts/build.sh"], {
  cwd: "tools",            // relative paths resolve against the project root
  env: { CI: "1" },        // added on top of the inherited environment
  quiet: false,            // set true to keep the program's output out of the build log
  timeout: 30_000,         // milliseconds; the child is killed and the task fails
});

result.ok;        // exited zero
result.exitCode;  // 0, 3, …
result.stdout;    // captured up to 1 MiB, even when not quiet; the rest is streamed only
result.stderr;    // under the same cap
```

`exec` se bloquea hasta que el programa termina y transmite su salida a la pantalla de compilación a medida que llega, atribuida a su tarea. No interviene ninguna shell: los argumentos llegan al programa exactamente como los escribió, así que nada en un nombre de archivo o una opción puede reinterpretarse como sintaxis de shell. Un nombre de comando simple se busca en el `PATH` con el que se ejecutará el proceso hijo (el que defina su `env`, si lo define), y un nombre que contiene un separador se resuelve contra `cwd`. Cancelar la compilación termina el programa y todo lo que haya iniciado. `timeout` limita el programa que nombró; una vez que termina, la salida que siga llegando se lee durante un segundo más y luego se descarta, así que un proceso en segundo plano que dejó con su salida abierta no retiene su tarea: a ese proceso ni se le espera ni se le termina.

También está disponible todo lo que ya tiene el JavaScript invitado (`node:fs`, `node:child_process`, `fetch`), así que use `exec` cuando quiera que la salida se atribuya a su tarea, y `node:child_process` cuando necesite algo que `exec` no cubre. Con eso viene una regla: un proceso hijo al que da la propia terminal de la compilación (`stdio: "inherit"`) escribe donde se dibuja la pantalla de progreso, así que cédale la terminal con `ctx.foreground`. Un proceso hijo cuya salida redirige e imprime usted mismo no necesita nada: la salida de su `console` pasa por la pantalla como cualquier otra.

### Ceder la terminal a un programa

```ts
const code = await ctx.foreground(
  () =>
    new Promise((resolve) => {
      const child = spawn(bin, args, { stdio: "inherit" });
      child.on("close", resolve);
    }),
);
```

La pantalla de progreso deja de dibujarse exactamente mientras se ejecuta la función, y vuelve termine como termine, así que la salida del programa y las filas de la pantalla nunca se borran entre sí. Una función `async` conserva la terminal hasta que su promesa se resuelve, y `await ctx.foreground(...)` devuelve lo que devolvió la función y vuelve a lanzar lo que lanzó.

Envuelva la cesión, no toda la tarea: todo lo que hace su tarea antes de lanzar el proceso (resolver rutas, leer archivos, llamar a `ctx.tools`) debe seguir mostrando progreso. Úselo solo cuando algo escribe en los descriptores propios de la compilación, es decir, un proceso hijo con stdio heredado o cualquier cosa interactiva, como un REPL o un depurador. La salida escrita con `ctx.log`, `ctx.exec`, `ctx.tools` o el `console` de su tarea ya está ordenada respecto a la pantalla.

## Usar la cadena de herramientas integrada

Elide ya incluye un compilador de Kotlin, `javac`, `jar`, Native Image, ktfmt y google-java-format. Una tarea que necesita uno no tiene que iniciar otro Elide para usarlo:

```ts
// Before: a new process per compile, diagnostics as text on a stream.
const compiled = ctx.exec("elide", ["kotlinc", "-d", ".dev/classes", "-cp", cp, "src/main/kotlin"]);
if (!compiled.ok) return ctx.fail(compiled.stderr);
ctx.exec("elide", ["jar", "--create", "--file", ".dev/lib/app.jar", "-C", ".dev/classes", "."]);

// After: the same tools, in-process, with structured results.
const result = ctx.tools.kotlinc({
  sources: ["src/main/kotlin"],                 // files, directories or globs, project-relative
  out: ".dev/classes",                          // a directory, or a path ending in `.jar`
  classpath: ctx.tools.classpath(),             // the resolved compile classpath, in order
  friendPaths: [".dev/classes-core"],           // whose `internal` declarations this compile may see
  args: ["-Xjvm-default=all"],                  // any further kotlinc flags, verbatim
});
if (!result.ok) {
  const first = result.diagnostics.find((d) => d.severity === "error");
  return ctx.fail(`${first?.file}:${first?.line}: ${first?.message}`);
}

ctx.tools.jar({
  out: ".dev/lib/app.jar",
  entries: [{ from: ".dev/classes" }, { from: "LICENSE", into: "META-INF" }],
  mainClass: "com.acme.MainKt",
  manifest: { "Implementation-Version": ctx.project.version ?? "dev" },
});
```

Cada llamada se bloquea hasta que la herramienta retorna y devuelve `{ ok, exitCode, tool, diagnostics }`. Los diagnósticos del compilador y del formateador se muestran en la salida de la compilación exactamente como los muestra una compilación integrada, y se devuelven en `diagnostics` como `{ severity, message, file, line, column }`; `jar` y `nativeImage` producen texto, que se transmite al registro de la compilación atribuido a su tarea (pase `quiet: true` para ocultarlo), y dejan `diagnostics` vacío. Las rutas son relativas al proyecto salvo que sean absolutas. Puede escribir salidas en cualquier lugar, pero solo las que están dentro del directorio del proyecto pueden almacenarse en caché y restaurarse, así que manténgalas ahí cuando la tarea las declare.

- `kotlinc({ sources, out, classpath?, friendPaths?, args?, incremental?, plugins? })`: el compilador de Kotlin integrado. Los plugins del compilador que su manifiesto declara o activa (`kotlin.features`, `kotlin.plugins`, `mavenDependencies().kotlinPlugins`) se aplican igual que en la compilación integrada, y los runtimes que aportan esas funciones (el de Metro, por ejemplo) se añaden al classpath con ellos: un plugin sin su runtime hace fallar al compilador en lugar de no hacer nada. `plugins: false` ejecuta el compilador sin plugins, que es lo que necesitan las fuentes con solo anotaciones. No se añade ningún `-jvm-target` salvo que lo pase en `args`.
- `javac({ sources, out, classpath?, processorPath?, args? })`: el compilador de Java integrado; un `processorPath` activa el procesamiento de anotaciones.
- `jar({ out, entries, exclude?, date?, manifest?, mainClass?, quiet? })`: cada entrada es un directorio (su contenido), un archivo u otro archivo comprimido cuyas entradas se toman (`archive: true`), y se coloca en la raíz del archivo o bajo `into`. `Manifest-Version` y `Created-By` siempre se escriben; `mainClass` se convierte en `Main-Class`.
- `nativeImage({ args, cwd?, quiet? })`: el controlador de Native Image integrado; las rutas relativas de `args` se anclan en `cwd`, que por defecto es la raíz del proyecto. El builder se ejecuta como una JVM hija fijada al propio JDK de Elide.
- `format({ tool, files, check?, args? })`: los formateadores integrados: `tool` es `"ktfmt"` para Kotlin o `"javaformat"` para google-java-format. `files` son archivos, directorios o globs, limitados a lo que acepta el formateador (`.kt`/`.kts` o `.java`); si no coincide nada, la llamada tiene éxito y no hace nada. Los archivos se reescriben in situ o, con `check: true`, solo se notifican, y la llamada falla si algún archivo cambiaría. En ambos casos `changed` enumera las rutas absolutas de los archivos que no estaban formateados, y cada uno llega como diagnóstico: una advertencia con `check`, un aviso informativo en una reescritura y un error para un archivo que no pudo leerse ni analizarse. `args` pasa más opciones al formateador, como `["--google-style"]`.
- `classpath({ sourceSet?, usage? })`: el classpath de dependencias resuelto como rutas absolutas en orden. `usage` es `"compile"` (predeterminado; lo que ven las tareas de compilación), `"runtime"`, `"test"` (el classpath de ejecución del conjunto de pruebas), `"processor"` o `"modules"`. Devuelve lo que esta compilación ha resuelto, así que una tarea que lo lee debería usar `dependsOn: "maven-dependencies"`.

`ctx.tools.version` es la versión de estas formas de opciones y resultados: hoy, `1`. Solo cambia con un cambio incompatible; una nueva opción o un nuevo miembro de resultado opcional no la cambia. Una extensión que debe funcionar con más de una versión de Elide se ramifica según su valor.

Los compiladores se ejecutan en el mismo proceso solo a través de la cadena de herramientas integrada. Un manifiesto que selecciona un `kotlin.toolchain` personalizado o gestionado, o el `jvm.java.compiler.mode` externo, recibe un error de `ctx.tools.kotlinc` / `ctx.tools.javac` que indica la tarea y la herramienta; elimine la selección o ejecute esa cadena de herramientas como programa con `ctx.exec`. Lo mismo ocurre con una distribución compilada sin la cadena de herramientas JVM.

### Reempaquetar un archivo

`jar` lee un archivo existente como origen, así que reducir una dependencia y sellarla de forma reproducible es una sola llamada en el mismo proceso en lugar de una cadena `unzip` / `rm -rf` / `jar`:

```ts
ctx.tools.jar({
  out: ".dev/lib/compiler-slim.jar",
  entries: [{ from: "libs/compiler.jar", archive: true }, { from: ".dev/patched" }],
  exclude: ["com/intellij/**", "META-INF/*.SF"],
  date: "2024-01-01T00:00:00Z",
});
```

`archive: true` nunca se infiere de la extensión del archivo: sin él, un `.jar` nombrado por `from` se añade como un único archivo. El `META-INF/MANIFEST.MF` del archivo de origen se descarta (el resultado lleva el manifiesto que describen `manifest` y `mainClass`, nunca el del origen) e `into` cambia la raíz de todo lo demás. Un miembro cuya ruta, tras `into`, quedaría fuera del archivo (`../x`, o un `/x` absoluto) hace fallar la tarea en lugar de escribirse, igual que un `into` que sale de él.

Los globs de `exclude` se comparan con la ruta que tendría una entrada *dentro del archivo*, después de que `into` haya cambiado su raíz. Son rutas, no nombres de paquete, así que un árbol de paquetes es `"kotlin/**"` y un tipo de archivo es `"META-INF/*.SF"`. Un glob en las opciones se aplica a todas las entradas sea cual sea su origen; un glob en una entrada se aplica solo a esa entrada, además de los anteriores.

`date` sella cada entrada, incluido el manifiesto, en ese instante, y la herramienta escribe los archivos de una raíz en orden, así que reempaquetar una entrada sin cambios produce un resultado idéntico byte a byte, que es lo que necesita un sello AOT o de distribución. Sin él, las fechas de las entradas no son reproducibles: una entrada que pasa sin cambios conserva la fecha de modificación de sus archivos, mientras que un miembro de un archivo, una entrada con `into` y una entrada afectada por una exclusión o una colisión llevan la hora de la llamada.

Las entradas se empaquetan en el orden en que las enumera, y dos de ellas pueden reclamar la misma ruta del archivo: gana la última, y la compilación avisa indicando la ruta y el origen del que se tomó. Nada se fusiona: un artefacto `Jar` declarado en el manifiesto une los archivos `META-INF/services` de sus raíces, `ctx.tools.jar` no, así que una tarea que necesita esa unión escribe ella misma el archivo fusionado y lo enumera en último lugar.
