@ressjs/vite-router 0.5.2 → 0.6.0-rc.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/LICENSE +69 -0
- package/README.md +9 -515
- package/dist/client/entry.d.mts +2 -0
- package/dist/client/entry.mjs +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/config/base-path.d.ts +44 -0
- package/dist/config/base-path.js +100 -0
- package/dist/config/codegen.d.ts +29 -0
- package/dist/config/codegen.js +131 -0
- package/dist/config/define.d.ts +8 -0
- package/dist/config/define.js +12 -0
- package/dist/config/env.d.ts +19 -0
- package/dist/config/env.js +40 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/config/index.js +46 -0
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +82 -0
- package/dist/config/middleware.d.ts +18 -0
- package/dist/config/middleware.js +79 -0
- package/dist/config/rules.d.ts +59 -0
- package/dist/config/rules.js +162 -0
- package/dist/config/types.d.ts +189 -0
- package/dist/config/types.js +2 -0
- package/dist/config/validate.d.ts +19 -0
- package/dist/config/validate.js +143 -0
- package/dist/config/watch.d.ts +53 -0
- package/dist/config/watch.js +163 -0
- package/dist/fs/module-extensions.d.ts +105 -0
- package/dist/fs/module-extensions.js +213 -0
- package/dist/head/resolve.d.ts +5 -0
- package/dist/head/resolve.js +166 -0
- package/dist/head/types.d.ts +44 -0
- package/dist/head/types.js +2 -0
- package/dist/helpers/html-generator.d.ts +26 -6
- package/dist/helpers/html-generator.js +96 -138
- package/dist/helpers/middlewares.d.ts +32 -21
- package/dist/helpers/middlewares.js +181 -159
- package/dist/helpers/page-config-merge.d.ts +11 -0
- package/dist/helpers/page-config-merge.js +84 -0
- package/dist/helpers/request-handler.d.ts +15 -2
- package/dist/helpers/request-handler.js +164 -27
- package/dist/index.d.ts +35 -3
- package/dist/index.js +77 -8
- package/dist/isr/capture.d.ts +79 -0
- package/dist/isr/capture.js +222 -0
- package/dist/isr/handler.d.ts +49 -0
- package/dist/isr/handler.js +207 -0
- package/dist/isr/key.d.ts +26 -0
- package/dist/isr/key.js +59 -0
- package/dist/isr/page-config.d.ts +17 -0
- package/dist/isr/page-config.js +71 -0
- package/dist/isr/preview.d.ts +5 -0
- package/dist/isr/preview.js +38 -0
- package/dist/isr/public-request.d.ts +50 -0
- package/dist/isr/public-request.js +108 -0
- package/dist/isr/response.d.ts +41 -0
- package/dist/isr/response.js +128 -0
- package/dist/isr/store.d.ts +42 -0
- package/dist/isr/store.js +108 -0
- package/dist/isr/types.d.ts +62 -0
- package/dist/isr/types.js +2 -0
- package/dist/pages.d.ts +10 -9
- package/dist/pages.js +42 -261
- package/dist/platform.d.ts +11 -14
- package/dist/platform.js +18 -102
- package/dist/plugin/environments.d.ts +68 -0
- package/dist/plugin/environments.js +74 -0
- package/dist/plugin/index.d.ts +59 -0
- package/dist/plugin/index.js +195 -0
- package/dist/plugin/route-manifest.d.ts +19 -0
- package/dist/plugin/route-manifest.js +55 -0
- package/dist/plugin/virtual-entries.d.ts +26 -0
- package/dist/plugin/virtual-entries.js +80 -0
- package/dist/render.d.ts +39 -2
- package/dist/render.js +75 -77
- package/dist/router.d.ts +32 -13
- package/dist/router.js +180 -146
- package/dist/routes/dispatcher.d.ts +58 -0
- package/dist/routes/dispatcher.js +70 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +62 -0
- package/dist/routes/match.d.ts +20 -0
- package/dist/routes/match.js +75 -0
- package/dist/routes/module.d.ts +14 -0
- package/dist/routes/module.js +30 -0
- package/dist/routes/parse.d.ts +31 -0
- package/dist/routes/parse.js +114 -0
- package/dist/routes/rank.d.ts +12 -0
- package/dist/routes/rank.js +49 -0
- package/dist/routes/router.d.ts +54 -0
- package/dist/routes/router.js +153 -0
- package/dist/routes/scan.d.ts +26 -0
- package/dist/routes/scan.js +98 -0
- package/dist/routes/types.d.ts +114 -0
- package/dist/routes/types.js +17 -0
- package/dist/runtime/dev-server.d.ts +64 -0
- package/dist/runtime/dev-server.js +94 -0
- package/dist/runtime/dev-styles.d.ts +13 -0
- package/dist/runtime/dev-styles.js +50 -0
- package/dist/runtime/module-loader.d.ts +55 -0
- package/dist/runtime/module-loader.js +122 -0
- package/dist/runtime/prod-server.d.ts +55 -0
- package/dist/runtime/prod-server.js +188 -0
- package/dist/runtime/template.d.ts +10 -0
- package/dist/runtime/template.js +33 -0
- package/dist/security/client-props.d.ts +25 -0
- package/dist/security/client-props.js +51 -0
- package/dist/security/config.d.ts +121 -0
- package/dist/security/config.js +77 -0
- package/dist/security/csp.d.ts +53 -0
- package/dist/security/csp.js +118 -0
- package/dist/security/dev-hardening.d.ts +46 -0
- package/dist/security/dev-hardening.js +65 -0
- package/dist/security/errors.d.ts +37 -0
- package/dist/security/errors.js +84 -0
- package/dist/security/escape.d.ts +40 -0
- package/dist/security/escape.js +90 -0
- package/dist/security/head-tags.d.ts +32 -0
- package/dist/security/head-tags.js +156 -0
- package/dist/security/headers.d.ts +76 -0
- package/dist/security/headers.js +278 -0
- package/dist/security/index.d.ts +24 -0
- package/dist/security/index.js +49 -0
- package/dist/security/serialize.d.ts +48 -0
- package/dist/security/serialize.js +146 -0
- package/dist/variants/assets.d.ts +33 -0
- package/dist/variants/assets.js +144 -0
- package/dist/variants/catalog.d.ts +33 -0
- package/dist/variants/catalog.js +79 -0
- package/dist/variants/platform-tokens.d.ts +9 -0
- package/dist/variants/platform-tokens.js +15 -0
- package/dist/variants/resolve.d.ts +40 -0
- package/dist/variants/resolve.js +90 -0
- package/dist/variants/suffix.d.ts +8 -0
- package/dist/variants/suffix.js +12 -0
- package/dist/variants/types.d.ts +72 -0
- package/dist/variants/types.js +2 -0
- package/package.json +23 -11
- package/dist/helpers/vite-config.d.ts +0 -10
- package/dist/helpers/vite-config.js +0 -90
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CaptureResponse = void 0;
|
|
4
|
+
exports.runCaptured = runCaptured;
|
|
5
|
+
exports.headerDelta = headerDelta;
|
|
6
|
+
exports.varyFields = varyFields;
|
|
7
|
+
exports.seedFrom = seedFrom;
|
|
8
|
+
const node_events_1 = require("node:events");
|
|
9
|
+
const node_http_1 = require("node:http");
|
|
10
|
+
const dispatcher_1 = require("../routes/dispatcher");
|
|
11
|
+
/** Cabeceras que describen el transporte de una respuesta, no su contenido. */
|
|
12
|
+
const TRANSPORT_HEADERS = new Set([
|
|
13
|
+
'content-length',
|
|
14
|
+
'content-encoding',
|
|
15
|
+
'transfer-encoding',
|
|
16
|
+
'connection',
|
|
17
|
+
'keep-alive',
|
|
18
|
+
'date',
|
|
19
|
+
'etag',
|
|
20
|
+
'x-ress-cache',
|
|
21
|
+
]);
|
|
22
|
+
const normalize = (value) => Array.isArray(value) ? value.map(String) : String(value);
|
|
23
|
+
/**
|
|
24
|
+
* Una respuesta sin socket.
|
|
25
|
+
*
|
|
26
|
+
* Implementa la parte de `express.Response` que usan el pipeline de render y
|
|
27
|
+
* `getServerSideProps`, y nada la conecta con la respuesta real: una generación
|
|
28
|
+
* en segundo plano no puede escribirle a un cliente que ya recibió su documento.
|
|
29
|
+
* La compresión tampoco la toca, así que lo que queda capturado es el HTML tal
|
|
30
|
+
* como lo produjo la página.
|
|
31
|
+
*/
|
|
32
|
+
class CaptureResponse extends node_events_1.EventEmitter {
|
|
33
|
+
req;
|
|
34
|
+
onEnd;
|
|
35
|
+
statusCode = 200;
|
|
36
|
+
statusMessage = '';
|
|
37
|
+
locals = {};
|
|
38
|
+
#headers = new Map();
|
|
39
|
+
#chunks = [];
|
|
40
|
+
#ended = false;
|
|
41
|
+
constructor(req, seed = {}, onEnd) {
|
|
42
|
+
super();
|
|
43
|
+
this.req = req;
|
|
44
|
+
this.onEnd = onEnd;
|
|
45
|
+
for (const [name, value] of Object.entries(seed))
|
|
46
|
+
this.setHeader(name, value);
|
|
47
|
+
}
|
|
48
|
+
get headersSent() { return this.#ended; }
|
|
49
|
+
get writableEnded() { return this.#ended; }
|
|
50
|
+
get finished() { return this.#ended; }
|
|
51
|
+
setHeader(name, value) {
|
|
52
|
+
this.#headers.set(name.toLowerCase(), { name, value: normalize(value) });
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
getHeader(name) { return this.#headers.get(name.toLowerCase())?.value; }
|
|
56
|
+
getHeaders() {
|
|
57
|
+
return Object.fromEntries([...this.#headers].map(([key, { value }]) => [key, value]));
|
|
58
|
+
}
|
|
59
|
+
getHeaderNames() { return [...this.#headers.keys()]; }
|
|
60
|
+
hasHeader(name) { return this.#headers.has(name.toLowerCase()); }
|
|
61
|
+
removeHeader(name) { this.#headers.delete(name.toLowerCase()); }
|
|
62
|
+
set(field, value) {
|
|
63
|
+
if (typeof field === 'object') {
|
|
64
|
+
for (const [name, v] of Object.entries(field))
|
|
65
|
+
this.setHeader(name, v);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
return this.setHeader(field, value);
|
|
69
|
+
}
|
|
70
|
+
header(field, value) { return this.set(field, value); }
|
|
71
|
+
get(name) { return this.getHeader(name); }
|
|
72
|
+
append(name, value) {
|
|
73
|
+
const previous = this.getHeader(name);
|
|
74
|
+
const values = [
|
|
75
|
+
...(previous === undefined ? [] : Array.isArray(previous) ? previous : [previous]),
|
|
76
|
+
...(Array.isArray(value) ? value.map(String) : [String(value)]),
|
|
77
|
+
];
|
|
78
|
+
return this.setHeader(name, values.length === 1 ? values[0] : values);
|
|
79
|
+
}
|
|
80
|
+
vary(field) {
|
|
81
|
+
const current = String(this.getHeader('Vary') ?? '');
|
|
82
|
+
const present = new Set(current.split(',').map((f) => f.trim().toLowerCase()).filter(Boolean));
|
|
83
|
+
if (present.has('*') || present.has(field.toLowerCase()))
|
|
84
|
+
return this;
|
|
85
|
+
return this.setHeader('Vary', current ? `${current}, ${field}` : field);
|
|
86
|
+
}
|
|
87
|
+
status(code) { this.statusCode = code; return this; }
|
|
88
|
+
sendStatus(code) { return this.status(code).type('text/plain').send(node_http_1.STATUS_CODES[code] ?? String(code)); }
|
|
89
|
+
type(value) {
|
|
90
|
+
const full = value.includes('/') ? value : value === 'html' ? 'text/html; charset=utf-8' : value === 'json' ? 'application/json; charset=utf-8' : `text/${value}`;
|
|
91
|
+
return this.setHeader('Content-Type', full);
|
|
92
|
+
}
|
|
93
|
+
contentType(value) { return this.type(value); }
|
|
94
|
+
location(url) { return this.setHeader('Location', url); }
|
|
95
|
+
redirect(statusOrUrl, maybeUrl) {
|
|
96
|
+
const [status, url] = typeof statusOrUrl === 'number' ? [statusOrUrl, maybeUrl ?? '/'] : [302, statusOrUrl];
|
|
97
|
+
this.status(status).location(url).end();
|
|
98
|
+
}
|
|
99
|
+
cookie(name, value, options = {}) {
|
|
100
|
+
const serialized = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value);
|
|
101
|
+
const attrs = [
|
|
102
|
+
`${name}=${encodeURIComponent(serialized)}`,
|
|
103
|
+
`Path=${options.path ?? '/'}`,
|
|
104
|
+
...(options.maxAge !== undefined ? [`Max-Age=${Math.floor(options.maxAge / 1000)}`] : []),
|
|
105
|
+
...(options.httpOnly ? ['HttpOnly'] : []),
|
|
106
|
+
...(options.secure ? ['Secure'] : []),
|
|
107
|
+
...(options.sameSite ? [`SameSite=${options.sameSite}`] : []),
|
|
108
|
+
];
|
|
109
|
+
return this.append('Set-Cookie', attrs.join('; '));
|
|
110
|
+
}
|
|
111
|
+
clearCookie(name, options = {}) {
|
|
112
|
+
return this.append('Set-Cookie', `${name}=; Path=${options.path ?? '/'}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
|
|
113
|
+
}
|
|
114
|
+
writeHead(status, headers) {
|
|
115
|
+
this.status(status);
|
|
116
|
+
if (headers)
|
|
117
|
+
this.set(headers);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
flushHeaders() { }
|
|
121
|
+
write(chunk) {
|
|
122
|
+
if (!this.#ended && chunk !== undefined && chunk !== null)
|
|
123
|
+
this.#chunks.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
end(chunk) {
|
|
127
|
+
if (this.#ended)
|
|
128
|
+
return this;
|
|
129
|
+
if (typeof chunk !== 'function')
|
|
130
|
+
this.write(chunk);
|
|
131
|
+
this.#ended = true;
|
|
132
|
+
this.emit('finish');
|
|
133
|
+
this.emit('close');
|
|
134
|
+
this.onEnd?.(this);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
send(body) {
|
|
138
|
+
if (body !== null && typeof body === 'object' && !Buffer.isBuffer(body))
|
|
139
|
+
return this.json(body);
|
|
140
|
+
if (!this.hasHeader('Content-Type')) {
|
|
141
|
+
this.setHeader('Content-Type', Buffer.isBuffer(body) ? 'application/octet-stream' : 'text/html; charset=utf-8');
|
|
142
|
+
}
|
|
143
|
+
return this.end(body);
|
|
144
|
+
}
|
|
145
|
+
json(body) {
|
|
146
|
+
if (!this.hasHeader('Content-Type'))
|
|
147
|
+
this.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
148
|
+
return this.end(JSON.stringify(body));
|
|
149
|
+
}
|
|
150
|
+
/** Lo que se escribió, sin las cabeceras de transporte. */
|
|
151
|
+
snapshot() {
|
|
152
|
+
const headers = this.getHeaders();
|
|
153
|
+
for (const name of TRANSPORT_HEADERS)
|
|
154
|
+
delete headers[name];
|
|
155
|
+
return { status: this.statusCode, headers, body: this.#chunks.join('') };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.CaptureResponse = CaptureResponse;
|
|
159
|
+
/**
|
|
160
|
+
* Ejecuta la cadena de render contra una respuesta sintética.
|
|
161
|
+
*
|
|
162
|
+
* Resuelve con lo capturado; con `null` si la cadena terminó sin responder
|
|
163
|
+
* (la ruta la atiende otro); rechaza con el error que la cadena propagó.
|
|
164
|
+
*/
|
|
165
|
+
function runCaptured(chain, req, seed) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const res = new CaptureResponse(req, seed, (done) => resolve(done.snapshot()));
|
|
168
|
+
req.res = res;
|
|
169
|
+
(0, dispatcher_1.runMiddlewareChain)(chain, req, res, ((error) => {
|
|
170
|
+
if (res.writableEnded)
|
|
171
|
+
return;
|
|
172
|
+
if (error)
|
|
173
|
+
reject(error);
|
|
174
|
+
else
|
|
175
|
+
resolve(null);
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Lo que la generación agregó o cambió respecto de la semilla.
|
|
181
|
+
*
|
|
182
|
+
* La semilla son las cabeceras que la respuesta real ya tenía —las de los
|
|
183
|
+
* middlewares—, y esas se recalculan en cada petición: guardarlas congelaría las
|
|
184
|
+
* de un visitante para todos. De `Vary` se guardan sólo los campos nuevos.
|
|
185
|
+
*/
|
|
186
|
+
function headerDelta(seed, produced) {
|
|
187
|
+
const delta = {};
|
|
188
|
+
for (const [name, value] of Object.entries(produced)) {
|
|
189
|
+
if (TRANSPORT_HEADERS.has(name))
|
|
190
|
+
continue;
|
|
191
|
+
const before = seed[name];
|
|
192
|
+
if (name === 'vary') {
|
|
193
|
+
const known = new Set(varyFields(before).map((field) => field.toLowerCase()));
|
|
194
|
+
const added = varyFields(value).filter((field) => !known.has(field.toLowerCase()));
|
|
195
|
+
if (added.length)
|
|
196
|
+
delta.vary = added.join(', ');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (before === undefined || String(before) !== String(value))
|
|
200
|
+
delta[name] = value;
|
|
201
|
+
}
|
|
202
|
+
return delta;
|
|
203
|
+
}
|
|
204
|
+
function varyFields(value) {
|
|
205
|
+
if (value === undefined)
|
|
206
|
+
return [];
|
|
207
|
+
return (Array.isArray(value) ? value.join(',') : value)
|
|
208
|
+
.split(',')
|
|
209
|
+
.map((field) => field.trim())
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
/** Las cabeceras de la respuesta real, normalizadas, sin cookies. */
|
|
213
|
+
function seedFrom(res) {
|
|
214
|
+
const seed = {};
|
|
215
|
+
for (const [name, value] of Object.entries(res.getHeaders())) {
|
|
216
|
+
const key = name.toLowerCase();
|
|
217
|
+
if (value === undefined || key === 'set-cookie' || TRANSPORT_HEADERS.has(key))
|
|
218
|
+
continue;
|
|
219
|
+
seed[key] = normalize(value);
|
|
220
|
+
}
|
|
221
|
+
return seed;
|
|
222
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Request, RequestHandler } from 'express';
|
|
2
|
+
import type { ResolvedIsrConfig } from '../config/types';
|
|
3
|
+
import type { RouteEntry } from '../routes/types';
|
|
4
|
+
import { type RequestPlatform } from './key';
|
|
5
|
+
import { type MemoryIsrStore } from './store';
|
|
6
|
+
import type { ResolvedPageRendering } from './types';
|
|
7
|
+
/** Lo que el router comparte con todos los manejadores ISR. */
|
|
8
|
+
export interface IsrRuntime {
|
|
9
|
+
store: MemoryIsrStore;
|
|
10
|
+
options: ResolvedIsrConfig;
|
|
11
|
+
/** En desarrollo sólo con `isr.dev`: si no, se genera en cada petición. */
|
|
12
|
+
cacheEnabled: boolean;
|
|
13
|
+
buildId: string;
|
|
14
|
+
/** La política de contenido lleva nonce: cambia qué se le dice a una CDN. */
|
|
15
|
+
usesNonce: boolean;
|
|
16
|
+
isProduction: boolean;
|
|
17
|
+
/** El `Cache-Control` de HTML por defecto, para cuando una entrada deja de declararse compartible. */
|
|
18
|
+
htmlCacheControl: string;
|
|
19
|
+
resolvePlatform(req: Request): RequestPlatform;
|
|
20
|
+
}
|
|
21
|
+
export interface IsrHandlerOptions {
|
|
22
|
+
entry: RouteEntry;
|
|
23
|
+
/** La del export estático. `getServerSideProps` puede cambiarla en cada generación. */
|
|
24
|
+
rendering: ResolvedPageRendering;
|
|
25
|
+
/** `getServerSideProps` y render: lo único que la caché evita ejecutar. */
|
|
26
|
+
render: readonly RequestHandler[];
|
|
27
|
+
/** La ruta tiene middlewares de carpeta: una CDN no los correría. */
|
|
28
|
+
hasMiddlewares?: boolean;
|
|
29
|
+
runtime: IsrRuntime;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* El manejador de render de una página.
|
|
33
|
+
*
|
|
34
|
+
* Va después de los middlewares de carpeta, que corren siempre con la petición
|
|
35
|
+
* real: si protegen la ruta, un acierto no los saltea. Lo que reemplaza es sólo
|
|
36
|
+
* `getServerSideProps` y el render.
|
|
37
|
+
*
|
|
38
|
+
* El modo de la ruta es el de su última generación. Empieza con el del export
|
|
39
|
+
* estático; si `getServerSideProps` devuelve otro, la ruta lo adopta desde la
|
|
40
|
+
* petición siguiente. Así una página puede declararse ISR desde cualquiera de
|
|
41
|
+
* los dos lugares: la caché se consulta antes de ejecutar nada, de modo que la
|
|
42
|
+
* primera petición de una ruta que sólo lo declara en `getServerSideProps`
|
|
43
|
+
* corre como SSR y es la que lo descubre.
|
|
44
|
+
*
|
|
45
|
+
* En modo ISR, `getServerSideProps` y el render se ejecutan contra una
|
|
46
|
+
* petición pública y una respuesta sintética. MISS, STALE y desarrollo
|
|
47
|
+
* recorren ese mismo camino.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createIsrHandler({ entry, rendering, render, runtime, hasMiddlewares }: IsrHandlerOptions): RequestHandler;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createIsrHandler = createIsrHandler;
|
|
4
|
+
const dispatcher_1 = require("../routes/dispatcher");
|
|
5
|
+
const capture_1 = require("./capture");
|
|
6
|
+
const key_1 = require("./key");
|
|
7
|
+
const preview_1 = require("./preview");
|
|
8
|
+
const public_request_1 = require("./public-request");
|
|
9
|
+
const response_1 = require("./response");
|
|
10
|
+
const store_1 = require("./store");
|
|
11
|
+
/**
|
|
12
|
+
* El manejador de render de una página.
|
|
13
|
+
*
|
|
14
|
+
* Va después de los middlewares de carpeta, que corren siempre con la petición
|
|
15
|
+
* real: si protegen la ruta, un acierto no los saltea. Lo que reemplaza es sólo
|
|
16
|
+
* `getServerSideProps` y el render.
|
|
17
|
+
*
|
|
18
|
+
* El modo de la ruta es el de su última generación. Empieza con el del export
|
|
19
|
+
* estático; si `getServerSideProps` devuelve otro, la ruta lo adopta desde la
|
|
20
|
+
* petición siguiente. Así una página puede declararse ISR desde cualquiera de
|
|
21
|
+
* los dos lugares: la caché se consulta antes de ejecutar nada, de modo que la
|
|
22
|
+
* primera petición de una ruta que sólo lo declara en `getServerSideProps`
|
|
23
|
+
* corre como SSR y es la que lo descubre.
|
|
24
|
+
*
|
|
25
|
+
* En modo ISR, `getServerSideProps` y el render se ejecutan contra una
|
|
26
|
+
* petición pública y una respuesta sintética. MISS, STALE y desarrollo
|
|
27
|
+
* recorren ese mismo camino.
|
|
28
|
+
*/
|
|
29
|
+
function createIsrHandler({ entry, rendering, render, runtime, hasMiddlewares = false }) {
|
|
30
|
+
const { store } = runtime;
|
|
31
|
+
// Una CDN no corre los middlewares de ress.js. Si la ruta tiene alguno, puede
|
|
32
|
+
// estar decidiendo quién ve la página; declararla compartible dejaría a la
|
|
33
|
+
// CDN servirle a cualquiera lo que guardó de alguien autorizado.
|
|
34
|
+
const { sharedCache } = runtime.options;
|
|
35
|
+
const shareable = sharedCache === 'always' || (sharedCache === 'auto' && !hasMiddlewares);
|
|
36
|
+
const sharedCacheControl = (config) => shareable ? (0, response_1.isrCacheControl)(config, runtime.usesNonce) : undefined;
|
|
37
|
+
const warned = new Set();
|
|
38
|
+
let current = rendering;
|
|
39
|
+
const warnOnce = (id, message) => {
|
|
40
|
+
if (runtime.isProduction || warned.has(id))
|
|
41
|
+
return;
|
|
42
|
+
warned.add(id);
|
|
43
|
+
console.warn(message);
|
|
44
|
+
};
|
|
45
|
+
return async (req, res, next) => {
|
|
46
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
47
|
+
return (0, dispatcher_1.runMiddlewareChain)(render, req, res, next);
|
|
48
|
+
}
|
|
49
|
+
if ((0, preview_1.isPreviewRequest)(req, runtime.options.previewSecret)) {
|
|
50
|
+
res.setHeader('X-Ress-Cache', 'BYPASS');
|
|
51
|
+
res.setHeader('Cache-Control', 'private, no-store');
|
|
52
|
+
return (0, dispatcher_1.runMiddlewareChain)(render, req, res, next);
|
|
53
|
+
}
|
|
54
|
+
// SSR con la petición real. Si su `getServerSideProps` declara otro modo,
|
|
55
|
+
// la ruta lo adopta desde la próxima petición.
|
|
56
|
+
if (current.mode !== 'isr')
|
|
57
|
+
return runLive(req, res, next);
|
|
58
|
+
const dropped = Object.keys(res.locals ?? {});
|
|
59
|
+
if (dropped.length) {
|
|
60
|
+
warnOnce(`locals:${dropped.join(',')}`, `[ress] ${entry.pattern} es ISR: estas claves de res.locals no llegan a la página, ` +
|
|
61
|
+
`porque su HTML se comparte entre visitantes: ${dropped.join(', ')}.`);
|
|
62
|
+
}
|
|
63
|
+
const platform = req.ressPlatform ?? runtime.resolvePlatform(req);
|
|
64
|
+
const { pathname } = new URL(req.url, 'http://ress.invalid');
|
|
65
|
+
const routeRendering = current;
|
|
66
|
+
const cacheControl = sharedCacheControl(routeRendering);
|
|
67
|
+
const origin = (0, public_request_1.requestOrigin)(req);
|
|
68
|
+
const { search, query } = (0, key_1.canonicalSearch)(req.url, routeRendering.searchParams);
|
|
69
|
+
const url = pathname + search;
|
|
70
|
+
const seed = (0, capture_1.seedFrom)(res);
|
|
71
|
+
const generate = async () => {
|
|
72
|
+
const publicReq = (0, public_request_1.createPublicRequest)(req, {
|
|
73
|
+
url,
|
|
74
|
+
query,
|
|
75
|
+
platform,
|
|
76
|
+
origin,
|
|
77
|
+
// La plataforma llega resuelta en `req.ressPlatform`, que es lo que
|
|
78
|
+
// separa la clave. El User-Agent crudo permitiría personalizar por
|
|
79
|
+
// navegador un HTML que se comparte entre todos los de esa plataforma.
|
|
80
|
+
includeUserAgent: false,
|
|
81
|
+
});
|
|
82
|
+
const generationSeed = cacheControl && seed['cache-control'] === undefined
|
|
83
|
+
? { ...seed, 'cache-control': cacheControl }
|
|
84
|
+
: seed;
|
|
85
|
+
const full = await (0, capture_1.runCaptured)(render, publicReq, generationSeed);
|
|
86
|
+
if (!full)
|
|
87
|
+
return null;
|
|
88
|
+
const generated = publicReq.ressPageRendering ?? routeRendering;
|
|
89
|
+
current = generated;
|
|
90
|
+
const delta = (0, capture_1.headerDelta)(seed, full.headers);
|
|
91
|
+
// `getServerSideProps` pudo cambiar los plazos de esta entrada: lo que se
|
|
92
|
+
// le dice a una CDN tiene que coincidir, salvo que la página haya puesto
|
|
93
|
+
// su propio `Cache-Control`.
|
|
94
|
+
if (generated !== routeRendering && cacheControl && full.headers['cache-control'] === cacheControl) {
|
|
95
|
+
const recalculated = sharedCacheControl(generated) ?? runtime.htmlCacheControl;
|
|
96
|
+
delta['cache-control'] = recalculated;
|
|
97
|
+
full.headers['cache-control'] = recalculated;
|
|
98
|
+
}
|
|
99
|
+
// Una CDN sólo puede guardar HTML que es igual para cualquier
|
|
100
|
+
// plataforma. ress.js separa por plataforma en su propia caché, pero la
|
|
101
|
+
// CDN no: en vez de pedirle que separe con `Vary` —muchas lo ignoran en
|
|
102
|
+
// HTML y, las que no, fragmentan la caché—, una página que depende de la
|
|
103
|
+
// plataforma simplemente no se le declara compartible. Depende si leyó
|
|
104
|
+
// una cabecera de plataforma o si tiene variantes (su `Vary` lo dice).
|
|
105
|
+
const dependsOnPlatform = (0, public_request_1.publicRequestUsage)(publicReq).readsPlatform || (0, capture_1.varyFields)(delta.vary).length > 0;
|
|
106
|
+
const ours = [cacheControl, sharedCacheControl(generated)].includes(full.headers['cache-control']);
|
|
107
|
+
if (dependsOnPlatform && ours && (0, response_1.isSharedCacheControl)(full.headers['cache-control'])) {
|
|
108
|
+
delta['cache-control'] = runtime.htmlCacheControl;
|
|
109
|
+
full.headers['cache-control'] = runtime.htmlCacheControl;
|
|
110
|
+
}
|
|
111
|
+
const verdict = generated.mode === 'isr'
|
|
112
|
+
? (0, response_1.cacheability)(full, delta)
|
|
113
|
+
: { ok: false, reason: `getServerSideProps declaró renderingMode: '${generated.mode}'` };
|
|
114
|
+
if (!verdict.ok && generated.mode === 'isr') {
|
|
115
|
+
warnOnce(`uncacheable:${verdict.reason}`, `[ress] ${entry.pattern} es ISR pero no se guarda: ${verdict.reason}.`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
template: (0, response_1.normalizeForCache)({ status: full.status, headers: delta, body: full.body }),
|
|
119
|
+
verdict,
|
|
120
|
+
rendering: generated,
|
|
121
|
+
committed: false,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
// La generación pública se hizo sin cookies ni credenciales: si la página
|
|
125
|
+
// resultó ser SSR, esa respuesta no le sirve a este visitante. Se descarta
|
|
126
|
+
// la entrada de esta URL y se atiende con la petición real.
|
|
127
|
+
const declaredSsr = (generation) => generation.rendering.mode !== 'isr';
|
|
128
|
+
const serve = (generation, state) => (0, response_1.applyCaptured)(res, (0, response_1.materialize)(generation.template), state);
|
|
129
|
+
if (!runtime.cacheEnabled) {
|
|
130
|
+
let generation;
|
|
131
|
+
try {
|
|
132
|
+
generation = await generate();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return next(error);
|
|
136
|
+
}
|
|
137
|
+
if (!generation)
|
|
138
|
+
return next();
|
|
139
|
+
if (declaredSsr(generation))
|
|
140
|
+
return runLive(req, res, next);
|
|
141
|
+
return serve(generation, 'BYPASS');
|
|
142
|
+
}
|
|
143
|
+
const key = (0, key_1.cacheKey)(runtime.buildId, (0, key_1.platformKeyOf)(platform), `${origin.protocol}://${origin.host}${url}`);
|
|
144
|
+
// Varias peticiones pueden esperar la misma generación: la primera que la
|
|
145
|
+
// recibe la guarda. Lo que devuelve una generación abandonada por tiempo no
|
|
146
|
+
// llega acá, así que nunca se guarda.
|
|
147
|
+
const commit = (generation) => {
|
|
148
|
+
if (generation.committed || !generation.verdict.ok)
|
|
149
|
+
return;
|
|
150
|
+
generation.committed = true;
|
|
151
|
+
store.set({
|
|
152
|
+
...generation.template,
|
|
153
|
+
key,
|
|
154
|
+
generatedAt: Date.now(),
|
|
155
|
+
revalidateMs: generation.rendering.revalidateMs,
|
|
156
|
+
expireMs: generation.rendering.expireMs,
|
|
157
|
+
bytes: Buffer.byteLength(generation.template.body),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const lookup = store.lookup(key);
|
|
161
|
+
if (lookup.state === 'fresh')
|
|
162
|
+
return (0, response_1.applyCaptured)(res, (0, response_1.materialize)(lookup.entry), 'HIT');
|
|
163
|
+
if (lookup.state === 'stale') {
|
|
164
|
+
(0, response_1.applyCaptured)(res, (0, response_1.materialize)(lookup.entry), 'STALE');
|
|
165
|
+
if (store.inFlight(key))
|
|
166
|
+
return;
|
|
167
|
+
store.run(key, generate).then((generation) => {
|
|
168
|
+
if (generation?.verdict.ok)
|
|
169
|
+
return commit(generation);
|
|
170
|
+
if (generation && declaredSsr(generation))
|
|
171
|
+
return store.delete(key);
|
|
172
|
+
const reason = generation && !generation.verdict.ok ? generation.verdict.reason : 'la cadena no respondió';
|
|
173
|
+
console.error(`[ress] La regeneración ISR de ${(0, store_1.readableKey)(key)} no produjo una entrada válida (${reason}); se conserva la anterior.`);
|
|
174
|
+
}, (error) => {
|
|
175
|
+
console.error(`[ress] Falló la regeneración ISR de ${(0, store_1.readableKey)(key)}; se conserva la anterior.`, error);
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Sin entrada, o más vieja que el techo: se genera antes de responder.
|
|
180
|
+
let generation;
|
|
181
|
+
try {
|
|
182
|
+
generation = await store.run(key, generate);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (lookup.state === 'expired')
|
|
186
|
+
store.delete(key);
|
|
187
|
+
return next(error);
|
|
188
|
+
}
|
|
189
|
+
if (!generation)
|
|
190
|
+
return next();
|
|
191
|
+
if (declaredSsr(generation)) {
|
|
192
|
+
store.delete(key);
|
|
193
|
+
return runLive(req, res, next);
|
|
194
|
+
}
|
|
195
|
+
commit(generation);
|
|
196
|
+
serve(generation, 'MISS');
|
|
197
|
+
};
|
|
198
|
+
function runLive(req, res, next) {
|
|
199
|
+
// `ressPageRendering` sólo existe si `getServerSideProps` devolvió campos
|
|
200
|
+
// de render; si no, la ruta conserva el modo que tenía.
|
|
201
|
+
res.once('finish', () => {
|
|
202
|
+
if (req.ressPageRendering)
|
|
203
|
+
current = req.ressPageRendering;
|
|
204
|
+
});
|
|
205
|
+
(0, dispatcher_1.runMiddlewareChain)(render, req, res, next);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlatformInfo } from '../platform';
|
|
2
|
+
export interface RequestPlatform {
|
|
3
|
+
platform: string;
|
|
4
|
+
platformInfo: PlatformInfo | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* La búsqueda que forma parte de la identidad de la página.
|
|
8
|
+
*
|
|
9
|
+
* Sólo sobreviven los parámetros declarados, ordenados por nombre; los valores
|
|
10
|
+
* repetidos conservan su orden porque `?tag=a&tag=b` y `?tag=b&tag=a` pueden
|
|
11
|
+
* significar cosas distintas. Lo no declarado desaparece: un `utm_source` no
|
|
12
|
+
* crea una entrada nueva ni llega a la página.
|
|
13
|
+
*/
|
|
14
|
+
export declare function canonicalSearch(url: string, allowed: readonly string[]): {
|
|
15
|
+
search: string;
|
|
16
|
+
query: Record<string, string | string[]>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* La plataforma como componente de la clave.
|
|
20
|
+
*
|
|
21
|
+
* Se usa la información completa y no sólo el nombre corto: el documento puede
|
|
22
|
+
* llevar la plataforma serializada, y dos clientes que se describen distinto no
|
|
23
|
+
* pueden compartir ese documento.
|
|
24
|
+
*/
|
|
25
|
+
export declare function platformKeyOf({ platform, platformInfo }: RequestPlatform): string;
|
|
26
|
+
export declare function cacheKey(buildId: string, platformKey: string, pathWithSearch: string): string;
|
package/dist/isr/key.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalSearch = canonicalSearch;
|
|
4
|
+
exports.platformKeyOf = platformKeyOf;
|
|
5
|
+
exports.cacheKey = cacheKey;
|
|
6
|
+
/**
|
|
7
|
+
* La búsqueda que forma parte de la identidad de la página.
|
|
8
|
+
*
|
|
9
|
+
* Sólo sobreviven los parámetros declarados, ordenados por nombre; los valores
|
|
10
|
+
* repetidos conservan su orden porque `?tag=a&tag=b` y `?tag=b&tag=a` pueden
|
|
11
|
+
* significar cosas distintas. Lo no declarado desaparece: un `utm_source` no
|
|
12
|
+
* crea una entrada nueva ni llega a la página.
|
|
13
|
+
*/
|
|
14
|
+
function canonicalSearch(url, allowed) {
|
|
15
|
+
if (allowed.length === 0)
|
|
16
|
+
return { search: '', query: {} };
|
|
17
|
+
const params = new URL(url, 'http://ress.invalid').searchParams;
|
|
18
|
+
const kept = new URLSearchParams();
|
|
19
|
+
const query = {};
|
|
20
|
+
for (const name of [...allowed].sort()) {
|
|
21
|
+
const values = params.getAll(name);
|
|
22
|
+
if (values.length === 0)
|
|
23
|
+
continue;
|
|
24
|
+
for (const value of values)
|
|
25
|
+
kept.append(name, value);
|
|
26
|
+
query[name] = values.length === 1 ? values[0] : values;
|
|
27
|
+
}
|
|
28
|
+
const search = kept.toString();
|
|
29
|
+
return { search: search ? `?${search}` : '', query };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* La plataforma como componente de la clave.
|
|
33
|
+
*
|
|
34
|
+
* Se usa la información completa y no sólo el nombre corto: el documento puede
|
|
35
|
+
* llevar la plataforma serializada, y dos clientes que se describen distinto no
|
|
36
|
+
* pueden compartir ese documento.
|
|
37
|
+
*/
|
|
38
|
+
function platformKeyOf({ platform, platformInfo }) {
|
|
39
|
+
if (!platformInfo)
|
|
40
|
+
return platform;
|
|
41
|
+
const { source: _deprecated, ...info } = platformInfo;
|
|
42
|
+
return JSON.stringify(sortKeys({
|
|
43
|
+
platform,
|
|
44
|
+
...info,
|
|
45
|
+
capabilities: [...info.capabilities].sort(),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
function cacheKey(buildId, platformKey, pathWithSearch) {
|
|
49
|
+
return `${buildId}\0${platformKey}\0${pathWithSearch}`;
|
|
50
|
+
}
|
|
51
|
+
function sortKeys(value) {
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.map(sortKeys);
|
|
54
|
+
if (!value || typeof value !== 'object')
|
|
55
|
+
return value;
|
|
56
|
+
return Object.fromEntries(Object.keys(value)
|
|
57
|
+
.sort()
|
|
58
|
+
.map((key) => [key, sortKeys(value[key])]));
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageRenderingConfig, ResolvedPageRendering } from './types';
|
|
2
|
+
/** Una configuración de página que no se puede usar, con todos sus problemas juntos. */
|
|
3
|
+
export declare class PageConfigError extends Error {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly problems: string[];
|
|
6
|
+
constructor(file: string, problems: string[]);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Valida la parte de render de `pageConfig` y la lleva a las unidades del runtime.
|
|
10
|
+
*
|
|
11
|
+
* Se acumulan todos los problemas antes de fallar, igual que la configuración
|
|
12
|
+
* del proyecto: corregir de a uno y reiniciar es una forma lenta de perder la tarde.
|
|
13
|
+
* `revalidate`, `expire` y `searchParams` se admiten con `ssr` y no tienen
|
|
14
|
+
* efecto: el export estático puede fijar los plazos y dejar que
|
|
15
|
+
* `getServerSideProps` decida el modo, sin que eso sea un error.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolvePageRendering(config: PageRenderingConfig | undefined, file: string): ResolvedPageRendering;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PageConfigError = void 0;
|
|
4
|
+
exports.resolvePageRendering = resolvePageRendering;
|
|
5
|
+
const MODES = ['ssr', 'isr'];
|
|
6
|
+
/** Una configuración de página que no se puede usar, con todos sus problemas juntos. */
|
|
7
|
+
class PageConfigError extends Error {
|
|
8
|
+
file;
|
|
9
|
+
problems;
|
|
10
|
+
constructor(file, problems) {
|
|
11
|
+
super(`[ress] ${file}: pageConfig tiene ${problems.length === 1 ? 'un problema' : `${problems.length} problemas`}:\n` +
|
|
12
|
+
problems.map((p) => ` · ${p}`).join('\n'));
|
|
13
|
+
this.file = file;
|
|
14
|
+
this.problems = problems;
|
|
15
|
+
this.name = 'PageConfigError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.PageConfigError = PageConfigError;
|
|
19
|
+
const isPositiveInteger = (value) => typeof value === 'number' && Number.isInteger(value) && value > 0;
|
|
20
|
+
/**
|
|
21
|
+
* Valida la parte de render de `pageConfig` y la lleva a las unidades del runtime.
|
|
22
|
+
*
|
|
23
|
+
* Se acumulan todos los problemas antes de fallar, igual que la configuración
|
|
24
|
+
* del proyecto: corregir de a uno y reiniciar es una forma lenta de perder la tarde.
|
|
25
|
+
* `revalidate`, `expire` y `searchParams` se admiten con `ssr` y no tienen
|
|
26
|
+
* efecto: el export estático puede fijar los plazos y dejar que
|
|
27
|
+
* `getServerSideProps` decida el modo, sin que eso sea un error.
|
|
28
|
+
*/
|
|
29
|
+
function resolvePageRendering(config, file) {
|
|
30
|
+
const { renderingMode, revalidate, expire, hydrate, searchParams } = config ?? {};
|
|
31
|
+
const problems = [];
|
|
32
|
+
if (renderingMode !== undefined && !MODES.includes(renderingMode)) {
|
|
33
|
+
problems.push(`\`renderingMode\` sólo acepta 'ssr' o 'isr'; se recibió ${JSON.stringify(renderingMode)}.`);
|
|
34
|
+
}
|
|
35
|
+
const mode = renderingMode === 'isr' ? 'isr' : 'ssr';
|
|
36
|
+
if (revalidate !== undefined && revalidate !== false && !isPositiveInteger(revalidate)) {
|
|
37
|
+
problems.push(revalidate === 0
|
|
38
|
+
? '`revalidate: 0` no es válido: una página que no se cachea usa renderingMode: \'ssr\'.'
|
|
39
|
+
: '`revalidate` tiene que ser un entero de segundos mayor que cero, o `false`.');
|
|
40
|
+
}
|
|
41
|
+
if (expire !== undefined) {
|
|
42
|
+
if (!isPositiveInteger(expire)) {
|
|
43
|
+
problems.push('`expire` tiene que ser un entero de segundos mayor que cero.');
|
|
44
|
+
}
|
|
45
|
+
else if (typeof revalidate !== 'number') {
|
|
46
|
+
problems.push('`expire` requiere un `revalidate` numérico: sin plazo de frescura no hay entrada vencida que acotar.');
|
|
47
|
+
}
|
|
48
|
+
else if (expire <= revalidate) {
|
|
49
|
+
problems.push(`\`expire\` (${expire}) tiene que ser mayor que \`revalidate\` (${revalidate}).`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (hydrate !== undefined && typeof hydrate !== 'boolean') {
|
|
53
|
+
problems.push('`hydrate` tiene que ser un booleano.');
|
|
54
|
+
}
|
|
55
|
+
if (searchParams !== undefined &&
|
|
56
|
+
(!Array.isArray(searchParams) || searchParams.some((name) => typeof name !== 'string' || !name))) {
|
|
57
|
+
problems.push('`searchParams` tiene que ser una lista de nombres de parámetro no vacíos.');
|
|
58
|
+
}
|
|
59
|
+
if (problems.length)
|
|
60
|
+
throw new PageConfigError(file, problems);
|
|
61
|
+
const seconds = typeof revalidate === 'number' ? revalidate : false;
|
|
62
|
+
return {
|
|
63
|
+
mode,
|
|
64
|
+
hydrate: hydrate !== false,
|
|
65
|
+
revalidate: seconds,
|
|
66
|
+
...(expire !== undefined ? { expire } : {}),
|
|
67
|
+
revalidateMs: seconds === false ? null : seconds * 1000,
|
|
68
|
+
expireMs: expire === undefined ? null : expire * 1000,
|
|
69
|
+
searchParams: [...new Set(searchParams ?? [])].sort(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
export declare const PREVIEW_HEADER = "x-ress-preview";
|
|
3
|
+
export declare const PREVIEW_COOKIE = "__ress_preview";
|
|
4
|
+
/** ¿La petición trae el secreto de vista previa? Sin secreto configurado, nunca. */
|
|
5
|
+
export declare function isPreviewRequest(req: Request, secret: string | undefined): boolean;
|