npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@griddo/cx-render-mock

v1.0.0

Published

Test-double del render de griddo-cx: núcleo de decisiones puro + motor de protocolo con transporte HTTP inyectable. Cero dependencias de runtime.

Readme

@griddo/cx-render-mock

Test-double del render de griddo-cx: resuelve los estados transitorios de las páginas (2 pending-publishing / 4 pending-unpublishing) a sus estados finales (3 live / 1 offline) llamando al protocolo build/startbuild/end de griddo-api, sin construir Gatsby ni subir a S3.

Cero dependencias de runtime: el HTTP se inyecta, así que sirve tanto en un script de Node como dentro de un runner de tests.

Publicación

Se publica en npm como @griddo/cx-render-mock con la action publish-cx-render-mock.yml, igual que api-types:

  • Por tag: git tag cx-render-mock-v0.1.0 && git push --tags. Sufijos -alpha/-beta/-rc publican con su dist-tag de prerelease.
  • A mano: pestaña Actions → "Release CX Render Mock" → Run workflow, indicando la versión.

El tarball lleva solo dist/, README.md, CHANGELOG.md y package.json (whitelist files; comprobable con npm pack --dry-run). La suite E2E (griddo-qa) mantiene una copia espejo hasta que consuma el paquete publicado. Racional completo de la extracción: griddo-qa/docs/cx-render-packaging-feasibility.md.

Contenido

| Módulo | Qué es | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | src/facade.ts | CxRenderMock: motor + publicador con una sola configuración, y el flujo completo. Empieza por aquí. | | src/engine.ts | CxRenderEngine: el protocolo de render, si solo necesitas esa parte. | | src/publish.ts | CmsPublisher: el disparo previo (marcar páginas, publicar sites). No es render, es lo que hace el CMS. | | src/core.ts | Funciones puras: clasificar por hash, diff por título, gate de build/start, body de build/end. Sin estado ni I/O. | | src/transport.ts | El contrato del HTTP inyectable y los decoradores withRetry / withTimeout. | | src/errors.ts | RenderError con code, para distinguir qué falló sin leer el mensaje. | | src/logger.ts | Contrato de log opcional (cuatro métodos, cero dependencias). |

Por qué esa división: el núcleo no tiene estado ni dependencias, así que son funciones puras testeables sin mocks; el motor sí tiene estado y una dependencia (el HTTP) que hay que invertir, así que es una clase con inyección por constructor.

Qué capa te toca usar

| Dónde lo consumes | Usa | | ------------------------------------------------------------ | ------------- | | Script, CLI o proceso de Node; test runner sobre Promises | el motor | | Runner con cola de comandos propia (Cypress y similares) | el núcleo |

Desde un script de Node: el motor

El transporte es la única pieza que escribes tú. Dos reglas: devuelve { status, body } y no lanza en 4xx/5xx, porque el motor necesita ver el status para decidir.

import { CxRenderMock, type RequestOptions, type Transport } from "@griddo/cx-render-mock";

const request = async (method: string, url: string, opts: RequestOptions = {}) => {
	const res = await fetch(url, {
		method,
		headers: opts.headers,
		body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
	});
	const text = await res.text();
	let body: unknown;
	try {
		body = text ? JSON.parse(text) : null;
	} catch {
		body = text; // un 502 con HTML, por ejemplo: el status ya informa
	}
	return { status: res.status, body };
};

const transport: Transport = {
	get: (url, opts) => request("GET", url, opts),
	post: (url, opts) => request("POST", url, opts),
	put: (url, opts) => request("PUT", url, opts),
};

const cx = new CxRenderMock({ transport, apiUrl, authToken });

// Publica, renderiza, confirma, y suelta el bloqueo de render si algo se rompe a medias.
await cx.publishAndRender([siteId], [pageId], ["Mi página"]);

Hay una versión ejecutable de esto en examples/render-a-page.mjs.

Si necesitas las piezas por separado, cx.engine y cx.publisher están ahí, o instáncialas tú (new CxRenderEngine({...}), new CmsPublisher({...})).

Si no sabes qué sites tienen trabajo pendiente, pregúntaselo al backend en lugar de llevar la cuenta:

await engine.renderPendingSites(); // GET sites/all → renderiza los que tengan cambios sin renderizar
await engine.renderPendingSites([siteId]); // acotado a los tuyos, si compartes base de datos con otros procesos

Ajustes

Todas las esperas y topes son tuyos; lo que no indiques toma el default (DEFAULT_TIMINGS):

new CxRenderEngine({
	transport,
	apiUrl,
	authToken,
	timings: { settleMs: 5_000, maxRounds: 5 }, // el resto sigue por defecto
	pageList: { itemsPerPage: 1_000 }, // sube el listado si tus sites tienen muchas páginas
	sleep: () => Promise.resolve(), // en tests, para no esperar de verdad
});

El núcleo acepta lo mismo por argumento: decideBuildStartStep(status, waits, timings) y listPageUrl(apiUrl, siteId, pageList).

Reintentos y timeouts

El transporte se puede envolver, así que esas políticas no viven en el motor:

import { withRetry, withTimeout } from "@griddo/cx-render-mock";

const transport = withRetry(withTimeout(myTransport, { ms: 30_000 }), { attempts: 4, logger });

withRetry solo reintenta lecturas por defecto, y solo ante 5xx o un fallo del cliente. No es timidez: build/start toma un bloqueo del site y build/end lo libera, así que repetir a ciegas una escritura puede dejar el protocolo a medias. Amplíalo con methods si tu caso lo tolera.

withTimeout deja de esperar, pero no cancela la request: el paquete no conoce tu cliente. Para cancelación real, usa un AbortController dentro de tu transporte.

Trazas

Pásale un logger y te cuenta qué hace (qué sites tienen trabajo, cuántas páginas publica, cuándo reintenta y por qué). Sin logger no escribe nada.

import { createConsoleLogger } from "@griddo/cx-render-mock";

const cx = new CxRenderMock({ transport, apiUrl, authToken, logger: createConsoleLogger("debug") });

Son cuatro métodos (debug, info, warn, error), así que encaja con console, con pino o con el log de tu runner de tests envolviéndolo.

Errores

Todo lo que lanza el paquete es un RenderError con un code, para no tener que leer el mensaje:

import { RENDER_ERROR_CODE, RenderError } from "@griddo/cx-render-mock";

try {
	await cx.publishAndRender([siteId], [pageId], titles);
} catch (error) {
	if (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {
		console.warn("no llegaron a su estado final:", error.missingTitles);
	}
}

Códigos: BUILD_START_NOT_READY, BUILD_END_FAILED, TITLES_NOT_FINAL, SITES_LOOKUP_FAILED, PAGE_LIST_FAILED, MARK_PENDING_FAILED, PUBLISH_FAILED. Los errores traen además status, body y la cause original cuando aplica.

⚠️ Solo se envuelven las respuestas con error. Si tu transporte revienta antes de haber respuesta (red caída, DNS, puerto cerrado), ese error se propaga tal cual, sin convertirse en RenderError: es tu cliente el que falló, no el protocolo. Maneja los dos casos.

Desde Cypress (o cualquier runner con cola): el núcleo

Orquesta tú las requests con el cliente del runner y delega solo las decisiones:

import { buildEndBody, classifyPage, decideBuildStartStep, listPageUrl } from "@griddo/cx-render-mock";

cy.request({ url: `${apiUrl}site/${siteId}/build/start`, failOnStatusCode: false }).then((bs) => {
	const step = decideBuildStartStep(bs.status, waits); // proceed | retry | fail
	// ...para cada id de bs.body.publishIds, pide GET page/{id} y decide con classifyPage(hash, status)
	cy.request({
		method: "POST",
		url: `${apiUrl}site/${siteId}/build/end`,
		body: buildEndBody(publishHashes, bs.body.siteHash, bs.body.unpublishHashes, publishPagesIds),
	});
});

Se puede meter el motor dentro de Cypress envolviéndolo en cy.then(() => engine.renderUntilLive(...)), pero no conviene, por dos motivos:

  1. Sus Promises no viven en la cola de comandos: pierdes el command log paso a paso y el reporte de fallo del runner queda menos preciso.
  2. Si el runner envuelve su propio cliente HTTP para inyectar cabeceras (Cypress lo hace sobreescribiendo cy.request), un transporte sobre fetch se las salta y ese tráfico deja de llevarlas.

Ambos caminos están probados: el motor con el paquete instalado desde el tarball en un consumidor de Node (ESM y CJS), y el núcleo con un spec real de Cypress consumiendo @griddo/cx-render-mock desde node_modules.

Dos avisos que importan

  1. Lock rendering: build/start bloquea el site y build/end lo libera. Si el proceso muere entre ambos, el site queda bloqueado ~4h. Llama a releaseRenderLock(siteId) en tu finally.
  2. No es seguro con varios procesos a la vez: build/end despublica copias globales de forma incondicional, sin acotar al site que lo llama. Dos procesos contra la misma base de datos se pisan; en griddo-api eso lo acota la feature qa-render-segregation, así que confirma que esté activa en tu entorno.

Limitaciones heredadas (conocidas, no arregladas)

  • Si la API responde con un body que no es un array ni { items } (por ejemplo, el HTML de un 5xx sin status de error), el recorrido puede romper con TypeError en lugar de un error con contexto.
  • El motor recorre las páginas de un site de una en una. Con muchas páginas es lento, pero es deliberado: ir en paralelo satura la API, y ese backend ya se ha caído por carga.

Referencia rápida

| Exporta | Qué es | | -------------------------------------------------------------------- | --------------------------------------------------------------------------- | | CxRenderMock | Fachada: engine, publisher y publishAndRender(siteIds, pageIds, titles, expectedStatus?) | | CxRenderEngine | renderUntilLive, renderPendingSites, renderSiteOnce, confirmPagesByTitle, releaseRenderLock | | CmsPublisher | markPagesPending, publishSites, publishPages | | Transport, TransportResponse, RequestOptions | El contrato del HTTP que inyectas | | withRetry, withTimeout | Decoradores del transporte | | RenderError, RENDER_ERROR_CODE | El error del paquete y sus códigos | | Logger, NOOP_LOGGER, createConsoleLogger | El contrato de log opcional | | RenderTimings, DEFAULT_TIMINGS, PageListQuery, DEFAULT_PAGE_LIST_QUERY | Esperas, topes y paginación | | LIVE_STATUS, PageInfo, SiteBuildInfo, BuildEndBody | Tipos del contrato de la API | | classifyPage, diffMissingByTitle, decideBuildStartStep, selectPendingSites, collectRepublishIds, buildEndBody, listPageUrl | Las decisiones puras, si orquestas tú |

Comandos

yarn install
yarn build            # dist/ (ESM + CJS + tipos) con tsup
yarn typecheck        # tsc sin emitir
yarn test             # tests unitarios (vitest)
yarn test:coverage    # los mismos, con informe de cobertura
yarn lint             # biome (formato + reglas)
yarn verify           # lint + typecheck + test, lo que corre antes de publicar

Cobertura actual: 98,75% de líneas y 90% de ramas (70 tests). Lo que queda sin cubrir son guardas defensivas para respuestas malformadas de la API.

Historial

Cambios por versión en CHANGELOG.md.

Antes de publicar

  1. Decidir registry y scope (npm privado / GitHub Packages) y quitar "private": true.
  2. Fijar quién mantiene el contrato de endBuild en griddo-api: si su semántica cambia, este paquete se rompe sin ninguna señal.
  3. Confirmar que qa-render-segregation está integrada en el entorno donde vaya a usarse.
  4. Probar el paquete empaquetado de verdad (yarn pack e instalarlo en un consumidor) antes del primer publish.
  5. Cuando esté publicado: cambiar el import de griddo-qa/cypress/support/cx-render.ts por el nombre del paquete y borrar griddo-qa/packages/cx-render.