Ir al contenido

Verbos de mutación

Todos los verbos de autoría viven en @pbuilder/sdk/commons — el mismo punto de entrada desde el que importa cada schematic. Cada verbo agenda una directiva; nada toca el disco hasta que el run hace flush (en el siguiente read(), o al final del run). Las escrituras a disco ocurren solo en la fase de apply del engine, después de que tu factory retorna — un error lanzado antes del retorno significa que no se escribe absolutamente nada.

Los siete verbos comparten una única unión cerrada de etiquetas AuthoringVerb: create, replaceContent, remove, rename, move, copy y copyIn — este último un hermano por-referencia de copy que copia directamente desde el paquete en lugar de renderizar una plantilla. (scaffold es una octava mutación, distribuida por separado, que se expande en directivas create/copyIn por entrada — tiene su propia guía.)

Verbo Qué agenda Retorna Sobre un destino existente
create Un archivo nuevo, renderizado desde una plantilla WritableHandle Rechaza (path-collision) salvo con { force: true }
replaceContent Reemplazo total del contenido de un archivo existente WritableHandle El destino debe existir — path-not-found en caso contrario
remove La eliminación de un archivo void Idempotente — nunca rechaza
rename Un renombrado solo del basename WritableHandle Rechaza (path-collision) salvo con { force: true }
move Un movimiento a otro directorio WritableHandle Rechaza (path-collision) salvo con { force: true }
copy Una copia de árbol a árbol WritableHandle Rechaza (path-collision) salvo con { force: true }
copyIn Un archivo local del paquete copiado al árbol tal cual void Rechaza (path-collision) salvo con { force: true }
modify Una edición estructural, consciente del AST — se alcanza a través de los dialectos handle de dialecto El destino debe existir y parsear

Los verbos que escriben en una ruta nueva son fail-closed ante colisiones (ante la duda, rechazan en lugar de sobrescribir) y aceptan { force: true } para una sobrescritura deliberada — create dentro de su objeto de opciones, el resto como argumento final. Los rechazos aparecen como un AuthoringError estructurado — consulta la guía de manejo de errores.

function create<S>(
path: string,
opts: { template: string; options: { [K in keyof S]: S[K] }; force?: boolean }
): WritableHandle;
function create(path: string, opts: { template: string; options: JsonValue; force?: boolean }): WritableHandle;
function create(
path: string,
opts: { templateFile: string; options: JsonValue; force?: boolean }
): WritableHandle;

Agenda una directiva de creación de archivo y retorna un WritableHandle para encadenar. path y template son ambos strings de plantilla, renderizados de forma independiente contra el mismo options; el mini-lenguaje de plantillas completo (delimitadores, los 7 pipes, loops, condicionales) vive en la guía de plantillas — esta página solo cubre la llamada en sí. El overload genérico S restringe options a las claves de un schema únicamente a nivel de tipos; el comportamiento en runtime es idéntico al overload plano.

import { create } from "@pbuilder/sdk/commons";
create("src/index.ts", {
template: "export const version = '{= .version =}';",
options: { version: "1.0.0" },
});

El tercer overload sustituye el string template inline por templateFile — una ruta local del paquete (resuelta contra el packageDir del run), leída en el momento de emisión; su contenido se convierte en el mismo campo template que el engine renderiza:

// Overload templateFile — lee la plantilla desde disco en lugar de escribirla inline
create("src/index.ts", {
templateFile: "index.ts.template",
options: { version: "1.0.0" },
});

Semántica de bordes y errores:

  • Una ruta de destino existente rechaza salvo que se pase { force: true } (fail-closed ante sobrescrituras) — AuthoringError con verb: "create", reason: "path-collision".
  • templateFile solo es usable dentro de un run iniciado con packageDir — de lo contrario no hay ancla de resolución (reason: "invalid-input", nunca un fallback silencioso al cwd). El CLI pasa packageDir automáticamente; en tests lo pasas tú (consulta Testing).
  • Un templateFile que es binario (un byte nulo o UTF-8 inválido en cualquier parte del archivo) o mayor que el límite de renderizado inline de 4 MiB falla ruidosamente con reason: "invalid-input" — nunca cae silenciosamente a una copia por-referencia.
  • Un templateFile que falta, no es un archivo regular o no se puede leer produce reason: "source-not-found" | "source-not-regular-file" | "source-unreadable" — las mismas tres razones que copyIn y scaffold comparten para sus propias lecturas locales del paquete. Un segmento literal ../ o una ruta absoluta en templateFile rechaza con reason: "invalid-input", antes de cualquier lectura (consulta la regla de fuentes locales del paquete más abajo).
function replaceContent(path: string, content: string): WritableHandle;

Agenda un reemplazo total, in-place, del contenido de un archivo existentecontent es un string crudo, no una plantilla. Un run rechazado (el destino no existe, reason: "path-not-found") lanza AuthoringError.

import { replaceContent } from "@pbuilder/sdk/commons";
replaceContent("src/config.json", '{ "version": "2.0.0" }');
function remove(path: string): void;

Agenda la eliminación de un archivo. Idempotente: eliminar un archivo ausente no es un error — en la práctica remove nunca rechaza.

import { remove } from "@pbuilder/sdk/commons";
remove("src/legacy.ts");
function rename(path: string, newName: string, opts?: { force?: boolean }): WritableHandle;

Agenda un renombrado solo del basename, retornando un handle para la nueva ruta (el directorio no cambia — solo se reemplaza el último segmento de la ruta). Renombrar sobre una ruta existente se rechaza salvo que se pase { force: true }reason: "path-collision".

import { rename } from "@pbuilder/sdk/commons";
rename("src/foo.ts", "bar.ts");
function move(path: string, toDir: string, opts?: { force?: boolean }): WritableHandle;

Agenda un movimiento a otro directorio, retornando un handle para la nueva ubicación. Mover sobre un destino existente se rechaza salvo con { force: true } (reason: "path-collision"); un movimiento cuyo destino es igual a su origen es un no-op, nunca una colisión.

import { move } from "@pbuilder/sdk/commons";
move("src/utils/helper.ts", "src/shared");
function copy(from: string, to: string, opts?: { force?: boolean }): WritableHandle;

Agenda una copia de árbol a árbol, retornando un handle sobre el que puedes encadenar más ediciones — el harness fake de tests hace staging de su contenido, así que un .read() encadenado sobre el handle retornado lo ve. Copiar sobre un destino existente se rechaza salvo con { force: true } (reason: "path-collision").

import { copy } from "@pbuilder/sdk/commons";
copy("src/template.ts", "src/generated/output.ts");
function copyIn(from: string, to: string, opts?: { force?: boolean }): void;

Copia UN archivo local del paquete (from, resuelto contra el packageDir del run) al árbol, siempre por-referencia — nunca se clasifica ni se renderiza, incluso cuando la fuente es texto plano que contiene secuencias con aspecto de plantilla. Es el hermano de copy para fuentes locales del paquete; contrástalo con create({ templateFile }), que explícitamente renderiza una fuente local del paquete.

import { copyIn } from "@pbuilder/sdk/commons";
copyIn("assets/logo.svg", "src/generated/logo.svg");

Semántica de bordes y errores:

  • from/to son obligatorios — si falta alguno, rechaza con reason: "invalid-input" antes de cualquier emisión.
  • Solo es usable dentro de un run iniciado con packageDir — de lo contrario reason: "invalid-input", nunca un fallback al cwd.
  • La fuente se filtra léxicamente (..//absoluta rechaza reason: "invalid-input" antes de leer), luego se valida su existencia y que sea un archivo regular, produciendo reason: "source-not-found" | "source-not-regular-file" | "source-unreadable" — las mismas tres razones que comparten create({ templateFile }) y scaffold.
  • Una colisión en el destino sin { force: true } rechaza con reason: "path-collision", verb: "copyIn" — el autor nunca llamó a copy, pero la etiqueta igualmente nombra la llamada realmente infractora.

La octava mutación es la que no viene de @pbuilder/sdk/commons — y es así por diseño. Todos los verbos anteriores tratan un archivo como texto; modify es una edición estructural, y para editar un archivo estructuralmente primero hay que entenderlo, lo que significa parsearlo a un AST. Ese entendimiento vive en los dialectos:

import * as ts from "@pbuilder/sdk/typescript";
await ts.find("src/index.ts")
.addImport("readFileSync", "node:fs") // op estructural con nombre
.modify((ast) => { // escape para todo lo demás
/* toda la superficie de ts-morph */
});

Sea cual sea la forma en que lo escribas — una op con nombre como addImport o el escape .modify() — toda la cadena se fusiona y llega al engine como una sola instrucción modify con el contenido final del archivo. El engine nunca ve el AST. replaceContent baja a la misma mutación de wire, y por eso sus errores reportan verb: "modify".

Consulta Modify para la superficie completa.

create({ templateFile }), copyIn y scaffold leen cada uno una fuente que vive en el disco del propio paquete, resuelta contra el packageDir del run. La regla para el autor:

el SDK rechaza rutas de fuente con ../ léxico o absolutas, siempre; todo lo que un schematic lee vive dentro de su paquete.

El SDK filtra la forma literal de la ruta (sin segmento .., sin forma absoluta) antes de tocar el disco — los symlinks se siguen sin verificar su destino, un residuo deliberado y documentado, cubierto en el SECURITY.md del SDK, no un descuido.

find(path) localiza un archivo existente y retorna un handle para leerlo o eliminarlo. read() se resuelve exactamente a uno de tres estados — nunca un chequeo de truthiness:

  • absent — la ruta no existe. read() se resuelve a undefined.
  • empty — el archivo existe pero su contenido es exactamente el string vacío "".
  • present — cualquier otro string, incluidos los de aspecto falsy como "0" o "false".
import { find, create, replaceContent } from "@pbuilder/sdk/commons";
const content = await find("src/config.ts").read();
if (content === undefined) {
create("src/config.ts", { template, options });
} else if (content === "") {
replaceContent("src/config.ts", seedContent);
} else {
replaceContent("src/config.ts", patch(content));
}

classifyContent() (también exportado desde @pbuilder/sdk/commons) nombra la tricotomía directamente, para un switch exhaustivo en lugar de comparaciones manuales:

import { classifyContent } from "@pbuilder/sdk/commons";
switch (classifyContent(content)) {
case "absent":
// ...
break;
case "empty":
// ...
break;
case "present":
// ...
break;
}

Las lecturas vienen del árbol de staging del engine, no del disco — así que leer una ruta que creaste antes en el mismo run ve el contenido en staging. Esa relectura es lo que convierte un schematic en una conversación: ediciones idempotentes y conscientes del contenido en lugar de sobrescrituras a ciegas.

Los factories se re-ejecutan contra proyectos ya generados, así que las mutaciones deben ser idempotentes: comprueba si tu marcador, import o entrada ya está presente antes de insertarlo. Los verbos están construidos para esta disciplina —

  • create es fail-closed sobre una ruta existente (path-collision): una re-ejecución nunca pisa silenciosamente la salida anterior. Recurre a { force: true } solo cuando sobrescribir es la intención.
  • find().read() te dice exactamente en qué estado está el árbol, para que puedas ramificar: crear cuando está ausente, sembrar cuando está vacío, parchear cuando está presente — el ejemplo de la tricotomía de arriba es la forma canónica.
  • remove ya es idempotente; una re-ejecución que elimina un archivo ya eliminado es un no-op.

Un factory de producción que agrega a una lista, por ejemplo, también omitiría el agregado cuando la entrada ya está listada — lee el contenido, comprueba la entrada, y solo entonces replaceContent. La guía de testing muestra cómo verificar la idempotencia con árboles sembrados.

  • Plantillas — el mini-lenguaje {= =} con el que create renderiza.
  • Scaffolding — replica una carpeta entera de plantillas con scaffold.
  • Dry-run — previsualiza los cambios planificados de un factory antes de que algo se confirme.
  • Manejo de errores — cómo luce AuthoringError y cómo escribir aserciones contra él.