@griddo/cx-render-mock 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +253 -0
- package/dist/index.d.mts +550 -0
- package/dist/index.d.ts +550 -0
- package/dist/index.js +591 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +544 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — sin publicar
|
|
4
|
+
|
|
5
|
+
Primera versión del paquete, extraída de la suite E2E de Griddo, donde este código lleva tiempo en uso.
|
|
6
|
+
|
|
7
|
+
- `CxRenderMock`: fachada con motor y publicador, y el flujo completo (`publishAndRender`).
|
|
8
|
+
- `CxRenderEngine`: `build/start` → clasificar por hash → `build/end`, con reintento por rondas,
|
|
9
|
+
descubrimiento de sites pendientes y liberación del bloqueo de render.
|
|
10
|
+
- `CmsPublisher`: marcar páginas como pendientes y publicar sites.
|
|
11
|
+
- Núcleo de decisiones puras, reutilizable si prefieres orquestar las requests tú.
|
|
12
|
+
- `Transport` inyectable con decoradores `withRetry` y `withTimeout`.
|
|
13
|
+
- `RenderError` con código, status, body y causa.
|
|
14
|
+
- Logger opcional de cuatro métodos.
|
|
15
|
+
- Esperas, topes y paginación configurables.
|
|
16
|
+
- Settle adaptativo: antes de `build/start` se sondea el listado hasta que dos firmas consecutivas coinciden
|
|
17
|
+
(`pageListSignature` + `decideSettleStep`), con `settleMs` como techo en lugar de como espera fija.
|
|
18
|
+
|
|
19
|
+
Notas de estructura de esta primera versión: las piezas que hablan con la API lo hacen a través de un cliente
|
|
20
|
+
interno (`api-client`), que es el único sitio donde se decide qué respuesta cuenta como fallo. El índice exporta
|
|
21
|
+
símbolo por símbolo, así que lo interno no se publica por accidente. Y dos métodos llevan el nombre de lo que
|
|
22
|
+
hacen hoy: `renderSiteOnce` (una sola pasada, ya no busca quiescencia) y `confirmPagesByTitle` (confirma
|
|
23
|
+
cualquier estado final, no solo `live`).
|
|
24
|
+
|
|
25
|
+
Pendiente antes de publicar: registry y scope, dueño del contrato de `endBuild` en griddo-api, y confirmar
|
|
26
|
+
`qa-render-segregation` en el entorno de uso. Ver el README.
|
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# @griddo/cx-render-mock
|
|
2
|
+
|
|
3
|
+
Test-double del render de **griddo-cx**: resuelve los estados transitorios de las páginas
|
|
4
|
+
(`2` pending-publishing / `4` pending-unpublishing) a sus estados finales (`3` live / `1` offline) llamando al
|
|
5
|
+
protocolo `build/start` → `build/end` de griddo-api, **sin** construir Gatsby ni subir a S3.
|
|
6
|
+
|
|
7
|
+
**Cero dependencias de runtime**: el HTTP se inyecta, así que sirve tanto en un script de Node como dentro de un
|
|
8
|
+
runner de tests.
|
|
9
|
+
|
|
10
|
+
## Publicación
|
|
11
|
+
|
|
12
|
+
Se publica en npm como `@griddo/cx-render-mock` con la action **`publish-cx-render-mock.yml`**, igual que
|
|
13
|
+
`api-types`:
|
|
14
|
+
|
|
15
|
+
- **Por tag**: `git tag cx-render-mock-v0.1.0 && git push --tags`. Sufijos `-alpha`/`-beta`/`-rc` publican con
|
|
16
|
+
su dist-tag de prerelease.
|
|
17
|
+
- **A mano**: pestaña Actions → "Release CX Render Mock" → Run workflow, indicando la versión.
|
|
18
|
+
|
|
19
|
+
El tarball lleva solo `dist/`, `README.md`, `CHANGELOG.md` y `package.json` (whitelist `files`; comprobable con
|
|
20
|
+
`npm pack --dry-run`). La suite E2E (griddo-qa) mantiene una copia espejo hasta que consuma el paquete
|
|
21
|
+
publicado. Racional completo de la extracción: `griddo-qa/docs/cx-render-packaging-feasibility.md`.
|
|
22
|
+
|
|
23
|
+
## Contenido
|
|
24
|
+
|
|
25
|
+
| Módulo | Qué es |
|
|
26
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| `src/facade.ts` | `CxRenderMock`: motor + publicador con una sola configuración, y el flujo completo. **Empieza por aquí.** |
|
|
28
|
+
| `src/engine.ts` | `CxRenderEngine`: el protocolo de render, si solo necesitas esa parte. |
|
|
29
|
+
| `src/publish.ts` | `CmsPublisher`: el disparo previo (marcar páginas, publicar sites). **No es render**, es lo que hace el CMS. |
|
|
30
|
+
| `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. |
|
|
31
|
+
| `src/transport.ts` | El contrato del HTTP inyectable y los decoradores `withRetry` / `withTimeout`. |
|
|
32
|
+
| `src/errors.ts` | `RenderError` con `code`, para distinguir qué falló sin leer el mensaje. |
|
|
33
|
+
| `src/logger.ts` | Contrato de log opcional (cuatro métodos, cero dependencias). |
|
|
34
|
+
|
|
35
|
+
Por qué esa división: el núcleo no tiene estado ni dependencias, así que son funciones puras testeables sin
|
|
36
|
+
mocks; el motor sí tiene estado y una dependencia (el HTTP) que hay que invertir, así que es una clase con
|
|
37
|
+
inyección por constructor.
|
|
38
|
+
|
|
39
|
+
## Qué capa te toca usar
|
|
40
|
+
|
|
41
|
+
| Dónde lo consumes | Usa |
|
|
42
|
+
| ------------------------------------------------------------ | ------------- |
|
|
43
|
+
| Script, CLI o proceso de Node; test runner sobre Promises | **el motor** |
|
|
44
|
+
| Runner con cola de comandos propia (Cypress y similares) | **el núcleo** |
|
|
45
|
+
|
|
46
|
+
### Desde un script de Node: el motor
|
|
47
|
+
|
|
48
|
+
El transporte es la única pieza que escribes tú. Dos reglas: devuelve `{ status, body }` y **no lanza** en
|
|
49
|
+
4xx/5xx, porque el motor necesita ver el status para decidir.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { CxRenderMock, type RequestOptions, type Transport } from "@griddo/cx-render-mock";
|
|
53
|
+
|
|
54
|
+
const request = async (method: string, url: string, opts: RequestOptions = {}) => {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers: opts.headers,
|
|
58
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
59
|
+
});
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
let body: unknown;
|
|
62
|
+
try {
|
|
63
|
+
body = text ? JSON.parse(text) : null;
|
|
64
|
+
} catch {
|
|
65
|
+
body = text; // un 502 con HTML, por ejemplo: el status ya informa
|
|
66
|
+
}
|
|
67
|
+
return { status: res.status, body };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const transport: Transport = {
|
|
71
|
+
get: (url, opts) => request("GET", url, opts),
|
|
72
|
+
post: (url, opts) => request("POST", url, opts),
|
|
73
|
+
put: (url, opts) => request("PUT", url, opts),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const cx = new CxRenderMock({ transport, apiUrl, authToken });
|
|
77
|
+
|
|
78
|
+
// Publica, renderiza, confirma, y suelta el bloqueo de render si algo se rompe a medias.
|
|
79
|
+
await cx.publishAndRender([siteId], [pageId], ["Mi página"]);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Hay una versión ejecutable de esto en [`examples/render-a-page.mjs`](examples/render-a-page.mjs).
|
|
83
|
+
|
|
84
|
+
Si necesitas las piezas por separado, `cx.engine` y `cx.publisher` están ahí, o instáncialas tú
|
|
85
|
+
(`new CxRenderEngine({...})`, `new CmsPublisher({...})`).
|
|
86
|
+
|
|
87
|
+
Si no sabes qué sites tienen trabajo pendiente, pregúntaselo al backend en lugar de llevar la cuenta:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await engine.renderPendingSites(); // GET sites/all → renderiza los que tengan cambios sin renderizar
|
|
91
|
+
await engine.renderPendingSites([siteId]); // acotado a los tuyos, si compartes base de datos con otros procesos
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Ajustes
|
|
95
|
+
|
|
96
|
+
Todas las esperas y topes son tuyos; lo que no indiques toma el default (`DEFAULT_TIMINGS`):
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
new CxRenderEngine({
|
|
100
|
+
transport,
|
|
101
|
+
apiUrl,
|
|
102
|
+
authToken,
|
|
103
|
+
timings: { settleMs: 5_000, maxRounds: 5 }, // el resto sigue por defecto
|
|
104
|
+
pageList: { itemsPerPage: 1_000 }, // sube el listado si tus sites tienen muchas páginas
|
|
105
|
+
sleep: () => Promise.resolve(), // en tests, para no esperar de verdad
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
El núcleo acepta lo mismo por argumento: `decideBuildStartStep(status, waits, timings)` y
|
|
110
|
+
`listPageUrl(apiUrl, siteId, pageList)`.
|
|
111
|
+
|
|
112
|
+
### Reintentos y timeouts
|
|
113
|
+
|
|
114
|
+
El transporte se puede envolver, así que esas políticas no viven en el motor:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { withRetry, withTimeout } from "@griddo/cx-render-mock";
|
|
118
|
+
|
|
119
|
+
const transport = withRetry(withTimeout(myTransport, { ms: 30_000 }), { attempts: 4, logger });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`withRetry` solo reintenta **lecturas** por defecto, y solo ante 5xx o un fallo del cliente. No es timidez:
|
|
123
|
+
`build/start` toma un bloqueo del site y `build/end` lo libera, así que repetir a ciegas una escritura puede
|
|
124
|
+
dejar el protocolo a medias. Amplíalo con `methods` si tu caso lo tolera.
|
|
125
|
+
|
|
126
|
+
`withTimeout` deja de esperar, pero **no cancela** la request: el paquete no conoce tu cliente. Para cancelación
|
|
127
|
+
real, usa un `AbortController` dentro de tu transporte.
|
|
128
|
+
|
|
129
|
+
### Trazas
|
|
130
|
+
|
|
131
|
+
Pásale un `logger` y te cuenta qué hace (qué sites tienen trabajo, cuántas páginas publica, cuándo reintenta y
|
|
132
|
+
por qué). Sin logger no escribe nada.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { createConsoleLogger } from "@griddo/cx-render-mock";
|
|
136
|
+
|
|
137
|
+
const cx = new CxRenderMock({ transport, apiUrl, authToken, logger: createConsoleLogger("debug") });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Son cuatro métodos (`debug`, `info`, `warn`, `error`), así que encaja con `console`, con pino o con el log de tu
|
|
141
|
+
runner de tests envolviéndolo.
|
|
142
|
+
|
|
143
|
+
### Errores
|
|
144
|
+
|
|
145
|
+
Todo lo que lanza el paquete es un `RenderError` con un `code`, para no tener que leer el mensaje:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { RENDER_ERROR_CODE, RenderError } from "@griddo/cx-render-mock";
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await cx.publishAndRender([siteId], [pageId], titles);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof RenderError && error.code === RENDER_ERROR_CODE.TITLES_NOT_FINAL) {
|
|
154
|
+
console.warn("no llegaron a su estado final:", error.missingTitles);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Códigos: `BUILD_START_NOT_READY`, `BUILD_END_FAILED`, `TITLES_NOT_FINAL`, `SITES_LOOKUP_FAILED`,
|
|
160
|
+
`PAGE_LIST_FAILED`, `MARK_PENDING_FAILED`, `PUBLISH_FAILED`. Los errores traen además `status`, `body` y la
|
|
161
|
+
`cause` original cuando aplica.
|
|
162
|
+
|
|
163
|
+
⚠️ Solo se envuelven las **respuestas** con error. Si tu transporte revienta antes de haber respuesta (red caída,
|
|
164
|
+
DNS, puerto cerrado), ese error se propaga tal cual, sin convertirse en `RenderError`: es tu cliente el que
|
|
165
|
+
falló, no el protocolo. Maneja los dos casos.
|
|
166
|
+
|
|
167
|
+
### Desde Cypress (o cualquier runner con cola): el núcleo
|
|
168
|
+
|
|
169
|
+
Orquesta tú las requests con el cliente del runner y delega solo las decisiones:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { buildEndBody, classifyPage, decideBuildStartStep, listPageUrl } from "@griddo/cx-render-mock";
|
|
173
|
+
|
|
174
|
+
cy.request({ url: `${apiUrl}site/${siteId}/build/start`, failOnStatusCode: false }).then((bs) => {
|
|
175
|
+
const step = decideBuildStartStep(bs.status, waits); // proceed | retry | fail
|
|
176
|
+
// ...para cada id de bs.body.publishIds, pide GET page/{id} y decide con classifyPage(hash, status)
|
|
177
|
+
cy.request({
|
|
178
|
+
method: "POST",
|
|
179
|
+
url: `${apiUrl}site/${siteId}/build/end`,
|
|
180
|
+
body: buildEndBody(publishHashes, bs.body.siteHash, bs.body.unpublishHashes, publishPagesIds),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Se puede meter el motor dentro de Cypress envolviéndolo en `cy.then(() => engine.renderUntilLive(...))`, pero
|
|
186
|
+
**no conviene**, por dos motivos:
|
|
187
|
+
|
|
188
|
+
1. Sus Promises no viven en la cola de comandos: pierdes el command log paso a paso y el reporte de fallo del
|
|
189
|
+
runner queda menos preciso.
|
|
190
|
+
2. Si el runner envuelve su propio cliente HTTP para inyectar cabeceras (Cypress lo hace sobreescribiendo
|
|
191
|
+
`cy.request`), un transporte sobre `fetch` se las salta y ese tráfico deja de llevarlas.
|
|
192
|
+
|
|
193
|
+
Ambos caminos están probados: el motor con el paquete instalado desde el tarball en un consumidor de Node (ESM y
|
|
194
|
+
CJS), y el núcleo con un spec real de Cypress consumiendo `@griddo/cx-render-mock` desde `node_modules`.
|
|
195
|
+
|
|
196
|
+
## Dos avisos que importan
|
|
197
|
+
|
|
198
|
+
1. **Lock `rendering`**: `build/start` bloquea el site y `build/end` lo libera. Si el proceso muere entre ambos,
|
|
199
|
+
el site queda bloqueado ~4h. Llama a `releaseRenderLock(siteId)` en tu `finally`.
|
|
200
|
+
2. **No es seguro con varios procesos a la vez**: `build/end` despublica copias globales de forma
|
|
201
|
+
incondicional, sin acotar al site que lo llama. Dos procesos contra la misma base de datos se pisan; en
|
|
202
|
+
griddo-api eso lo acota la feature `qa-render-segregation`, así que confirma que esté activa en tu entorno.
|
|
203
|
+
|
|
204
|
+
## Limitaciones heredadas (conocidas, no arregladas)
|
|
205
|
+
|
|
206
|
+
- Si la API responde con un body que no es un array ni `{ items }` (por ejemplo, el HTML de un 5xx sin status de
|
|
207
|
+
error), el recorrido puede romper con `TypeError` en lugar de un error con contexto.
|
|
208
|
+
- El motor recorre las páginas de un site de una en una. Con muchas páginas es lento, pero es deliberado: ir en
|
|
209
|
+
paralelo satura la API, y ese backend ya se ha caído por carga.
|
|
210
|
+
|
|
211
|
+
## Referencia rápida
|
|
212
|
+
|
|
213
|
+
| Exporta | Qué es |
|
|
214
|
+
| -------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
215
|
+
| `CxRenderMock` | Fachada: `engine`, `publisher` y `publishAndRender(siteIds, pageIds, titles, expectedStatus?)` |
|
|
216
|
+
| `CxRenderEngine` | `renderUntilLive`, `renderPendingSites`, `renderSiteOnce`, `confirmPagesByTitle`, `releaseRenderLock` |
|
|
217
|
+
| `CmsPublisher` | `markPagesPending`, `publishSites`, `publishPages` |
|
|
218
|
+
| `Transport`, `TransportResponse`, `RequestOptions` | El contrato del HTTP que inyectas |
|
|
219
|
+
| `withRetry`, `withTimeout` | Decoradores del transporte |
|
|
220
|
+
| `RenderError`, `RENDER_ERROR_CODE` | El error del paquete y sus códigos |
|
|
221
|
+
| `Logger`, `NOOP_LOGGER`, `createConsoleLogger` | El contrato de log opcional |
|
|
222
|
+
| `RenderTimings`, `DEFAULT_TIMINGS`, `PageListQuery`, `DEFAULT_PAGE_LIST_QUERY` | Esperas, topes y paginación |
|
|
223
|
+
| `LIVE_STATUS`, `PageInfo`, `SiteBuildInfo`, `BuildEndBody` | Tipos del contrato de la API |
|
|
224
|
+
| `classifyPage`, `diffMissingByTitle`, `decideBuildStartStep`, `selectPendingSites`, `collectRepublishIds`, `buildEndBody`, `listPageUrl` | Las decisiones puras, si orquestas tú |
|
|
225
|
+
|
|
226
|
+
## Comandos
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
yarn install
|
|
230
|
+
yarn build # dist/ (ESM + CJS + tipos) con tsup
|
|
231
|
+
yarn typecheck # tsc sin emitir
|
|
232
|
+
yarn test # tests unitarios (vitest)
|
|
233
|
+
yarn test:coverage # los mismos, con informe de cobertura
|
|
234
|
+
yarn lint # biome (formato + reglas)
|
|
235
|
+
yarn verify # lint + typecheck + test, lo que corre antes de publicar
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Cobertura actual: **98,75% de líneas y 90% de ramas** (70 tests). Lo que queda sin cubrir son guardas defensivas
|
|
239
|
+
para respuestas malformadas de la API.
|
|
240
|
+
|
|
241
|
+
## Historial
|
|
242
|
+
|
|
243
|
+
Cambios por versión en [CHANGELOG.md](CHANGELOG.md).
|
|
244
|
+
|
|
245
|
+
## Antes de publicar
|
|
246
|
+
|
|
247
|
+
1. Decidir **registry y scope** (npm privado / GitHub Packages) y quitar `"private": true`.
|
|
248
|
+
2. Fijar **quién mantiene el contrato de `endBuild`** en griddo-api: si su semántica cambia, este paquete se
|
|
249
|
+
rompe sin ninguna señal.
|
|
250
|
+
3. Confirmar que **`qa-render-segregation`** está integrada en el entorno donde vaya a usarse.
|
|
251
|
+
4. Probar el paquete empaquetado de verdad (`yarn pack` e instalarlo en un consumidor) antes del primer publish.
|
|
252
|
+
5. Cuando esté publicado: cambiar el import de `griddo-qa/cypress/support/cx-render.ts` por el nombre del
|
|
253
|
+
paquete y borrar `griddo-qa/packages/cx-render`.
|