Construyendo Pry: un visor de JSON nativo en TypeScript
Pry es un visor JSON nativo construido completamente en TypeScript y compilado con Perry. No es una demo técnica — es una herramienta real que usamos a diario para inspeccionar respuestas de API, archivos de configuración y volcados de datos. Este artículo recorre cómo fue construido, cómo compila y cómo es la experiencia de desarrollo cuando tu TypeScript compila a una app nativa.
Qué hace Pry
Pry lee un archivo JSON (o acepta JSON desde stdin) y lo renderiza como un árbol interactivo y navegable en una ventana nativa. Si has usado el Quick Look integrado de macOS para JSON, imagina eso — pero más rápido, con búsqueda y con navegación por teclado.
El conjunto de funcionalidades:
- Vista de árbol — nodos plegables para objetos y arrays, con indicadores de profundidad y expandir/colapsar todo
- Búsqueda — búsqueda de texto completo en claves y valores con resaltado en tiempo real y navegación de coincidencias
- Atajos de teclado — flechas para navegar, enter para expandir/colapsar, barra para buscar,
⌘Cpara copiar - Portapapeles — copiar cualquier nodo o subárbol como JSON formateado
- Coloreo de sintaxis — strings en verde, números en naranja, booleanos en púrpura, null en rojo
- Barra de estado — muestra el conteo total de nodos, profundidad actual, tamaño de archivo y tiempo de análisis
El código fuente
Pry está escrito en TypeScript estándar. No hay sintaxis especial, no hay macros, no hay generación de código en tiempo de compilación. Usa la API de UI de Perry, que proporciona widgets nativos que compilan a código específico de plataforma.
Aquí está el punto de entrada (simplificado para claridad):
import { App, VStack, TreeView, SearchBar, StatusBar, State }
from "perry/ui";
import { readFile, readStdin } from "perry/fs";
// Leer entrada desde argumento de archivo o stdin
const input = process.argv[2]
? readFile(process.argv[2])
: readStdin();
const startTime = Date.now();
const data = JSON.parse(input);
const parseMs = Date.now() - startTime;
// Estado reactivo
const searchQuery = new State("");
const matchCount = new State(0);
// Construir la app
const app = new App("Pry", {
width: 800,
height: 600,
minWidth: 400,
minHeight: 300,
});
app.body(() => {
return VStack({ spacing: 0 }, [
SearchBar({
placeholder: "Search keys and values...",
onSearch: (q) => searchQuery.value = q,
}),
TreeView(data, {
collapsible: true,
syntaxHighlight: true,
searchQuery: searchQuery,
onMatchCount: (n) => matchCount.value = n,
copyOnClick: true,
}),
StatusBar([
`${countNodes(data)} nodes`,
`Parsed in ${parseMs}ms`,
`${matchCount.value} matches`,
]),
]);
});
app.registerShortcut("/", () => app.focusSearchBar());
app.registerShortcut("Escape", () => {
searchQuery.value = "";
app.focusTree();
});
app.run();
Ese es el núcleo de una aplicación nativa. Sin boilerplate de framework, sin configuración de compilación, sin archivos específicos de plataforma. Un archivo TypeScript.
Las funciones auxiliares
Pry también incluye una utilidad countNodes que cuenta recursivamente todos los nodos en el árbol JSON, y un helper formatBytes para mostrar tamaños de archivo. Son funciones TypeScript estándar — nada específico de Perry en ellas. Compilan a código nativo igual que todo lo demás.
export function countNodes(data: unknown): number {
if (data === null || typeof data !== "object") {
return 1;
}
if (Array.isArray(data)) {
return 1 + data.reduce((sum, item) => sum + countNodes(item), 0);
}
const values = Object.values(data as Record<string, unknown>);
return 1 + values.reduce((sum, val) => sum + countNodes(val), 0);
}
Compilando Pry
Compilar Pry con Perry es un solo comando. Sin proyecto Xcode, sin configuración Gradle, sin config de webpack. Solo apuntar Perry al archivo de entrada y especificar tu objetivo.
macOS (ARM64)
$ perry build pry.ts --target macos-arm64
Parsing pry.ts...
Resolving imports: perry/ui, perry/fs
Compiling (cranelift, arm64)...
Linking with AppKit.framework...
✓ Built executable: pry (48 MB)
$ file pry
pry: Mach-O 64-bit executable arm64
$ otool -L pry | head -5
pry:
/System/Library/Frameworks/AppKit.framework/AppKit
/System/Library/Frameworks/Foundation.framework/Foundation
/usr/lib/libSystem.B.dylib
El binario tiene 48 MB porque incluye el stack completo de UI AppKit — renderizado de vista de árbol, resaltado de búsqueda, coloreo de sintaxis y manejo de teclado. Para comparación, la misma app en Electron sería de más de 200 MB. Una app Perry solo CLI compila a 2-5 MB.
iOS
$ perry build pry.ts --target ios-arm64
✓ Built executable: pry (52 MB)
La compilación iOS enlaza contra UIKit en lugar de AppKit. Perry mapea la misma API TreeView a UITableView con secciones expandibles, SearchBar a UISearchBar, y los eventos táctiles reemplazan los eventos de ratón. La compilación iOS puede desplegarse en dispositivos físicos y simuladores.
Android
$ perry build pry.ts --target android-arm64
✓ Built: pry.apk
La compilación Android genera una biblioteca nativa cargada a través de JNI, empaquetada en un APK. TreeView mapea a un RecyclerView con view holders expandibles, SearchBar mapea a un EditText con un TextWatcher, y la barra de estado mapea a un TextView en la parte inferior del layout.
Qué pasa bajo el capó
Cuando Perry compila Pry, pasa por varias fases:
- Análisis — SWC analiza el código fuente TypeScript en un AST. Los imports de
perry/uiyperry/fsse resuelven a las implementaciones de módulos incorporados de Perry. - Análisis de tipos — Perry resuelve todos los tipos, incluyendo los genéricos
State<string>yState<number>, monomorfizándolos en tipos concretos. - Resolución de plataforma — Basándose en el flag de objetivo, Perry selecciona el backend de UI apropiado. Cada llamada a
TreeView,SearchBaryButtonse resuelve a la implementación específica de la plataforma. - Generación de IR — Perry genera una representación intermedia que incluye llamadas a API nativas — envíos de mensajes Objective-C para macOS/iOS, llamadas JNI para Android, llamadas a funciones C para GTK4/Win32.
- Generación de código — Cranelift compila la IR a código máquina nativo para la arquitectura objetivo.
- Enlace — El código nativo se enlaza contra los frameworks de la plataforma (AppKit, UIKit, Android NDK, GTK4 o Win32) para producir el ejecutable final.
Sin runtime, sin vistas web
Esto vale la pena enfatizarlo porque es la diferencia central entre Perry y cualquier otro enfoque TypeScript-a-nativo. El binario compilado de Pry tiene:
- Sin motor JavaScript — sin V8, sin Hermes, sin JavaScriptCore
- Sin vistas web — sin Chromium, sin WebKit, sin WKWebView
- Sin capa bridge — sin mensajes serializados entre JS y nativo
- Sin runtime de framework — sin React, sin motor Flutter, sin VM Dart
El binario llama a las APIs de la plataforma directamente. En macOS, llama a objc_msgSend para interactuar con objetos AppKit. En Android, llama a funciones JNI para crear y manipular Views. Es lo mismo que haría una app nativa Swift o Kotlin.
La consecuencia práctica: Pry se lanza instantáneamente. No hay arranque de VM, no hay calentamiento JIT, no hay análisis de scripts. El proceso arranca, la ventana aparece, el JSON se renderiza. El uso de memoria es una fracción de lo que un equivalente Electron consumiría.
Experiencia de desarrollo
Construir Pry se sintió notablemente similar a construir cualquier aplicación TypeScript. El flujo de trabajo es:
- Escribir TypeScript en tu editor (VS Code, Zed, Neovim, lo que prefieras)
- Ejecutar
perry compile pry.ts - Ejecutar
./pry test.json - Iterar
Sin proyecto Xcode que configurar. Sin Android Studio que instalar. Sin compilación Gradle de 45 segundos. El compilador Perry en sí es rápido — analizar y compilar Pry toma unos pocos segundos, y estamos trabajando activamente en hacerlo más rápido.
El TypeScript que escribes es TypeScript estándar. La verificación de tipos de tu editor, el autocompletado y las herramientas de refactorización funcionan todas. Puedes extraer funciones, crear módulos, usar genéricos — todos los patrones TypeScript que ya conoces.
Qué aprendimos
Construir Pry nos enseñó mucho sobre lo que la API de UI de Perry necesita soportar. Algunas lecciones:
- Las vistas de árbol son complejas. Expandir, colapsar, resaltado de búsqueda, navegación por teclado e integración con el portapapeles necesitan estar coordinados. El widget
TreeViewde Perry maneja esto internamente, pero tuvimos que asegurarnos de que la implementación nativa fuera consistente en las tres plataformas. - Los atajos de teclado necesitan convenciones de plataforma. En macOS, es
⌘Cpara copiar. En Linux y Android, esCtrl+C. El sistema de atajos de Perry abstrae esto, pero requirió una implementación cuidadosa para hacerlo bien. - Las barras de estado son sorprendentemente no triviales. Cada plataforma tiene una convención diferente sobre dónde y cómo mostrar información de estado. AppKit usa la barra inferior de la ventana, UIKit usa una toolbar, Android usa una view inferior en el layout. La
StatusBarde Perry mapea a cada una correctamente. - El soporte de stdin requirió conciencia de plataforma. En macOS y Linux, leer de stdin es directo. En iOS y Android, stdin no "existe" realmente de la misma manera, así que Pry usa selección de archivos en plataformas móviles. El
readStdinde Perry maneja esto de forma transparente.
Rendimiento
Pry maneja archivos JSON grandes cómodamente. En nuestras pruebas:
- Un archivo JSON de 1 MB (más de 10.000 nodos) analiza y renderiza en menos de 50 ms
- Un archivo JSON de 10 MB renderiza en menos de 200 ms
- La búsqueda en 10.000 nodos devuelve resultados mientras escribes, sin retraso visible
- El uso de memoria se mantiene por debajo de 50 MB incluso para archivos grandes
Esta es la ventaja de la compilación nativa. El análisis JSON en Perry se compila a bucles nativos ajustados sin pausas de GC. El renderizado del árbol usa las vistas de lista virtualizadas propias de la plataforma (NSOutlineView, UITableView, RecyclerView), que están probadas en batalla para rendimiento.
Código fuente y descargas
Pry es open source. Puedes explorar el código fuente completo, compilarlo tú mismo, o simplemente mirar el código para entender cómo está estructurada una app de UI nativa de Perry.
- Repositorio GitHub — código fuente completo e instrucciones de compilación
- Página del showcase — capturas de pantalla, lista de funcionalidades y detalles de plataforma
Si estás construyendo algo con Perry, nos encantaría saberlo. Abre un issue en el repositorio de Perry o inicia una discusión. Estamos construyendo Perry de forma abierta y el feedback de usuarios reales construyendo apps reales es invaluable.