@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.
Files changed (141) hide show
  1. package/LICENSE +69 -0
  2. package/README.md +9 -515
  3. package/dist/client/entry.d.mts +2 -0
  4. package/dist/client/entry.mjs +1 -0
  5. package/dist/client/index.d.ts +2 -0
  6. package/dist/client/index.js +8 -0
  7. package/dist/config/base-path.d.ts +44 -0
  8. package/dist/config/base-path.js +100 -0
  9. package/dist/config/codegen.d.ts +29 -0
  10. package/dist/config/codegen.js +131 -0
  11. package/dist/config/define.d.ts +8 -0
  12. package/dist/config/define.js +12 -0
  13. package/dist/config/env.d.ts +19 -0
  14. package/dist/config/env.js +40 -0
  15. package/dist/config/index.d.ts +18 -0
  16. package/dist/config/index.js +46 -0
  17. package/dist/config/load.d.ts +28 -0
  18. package/dist/config/load.js +82 -0
  19. package/dist/config/middleware.d.ts +18 -0
  20. package/dist/config/middleware.js +79 -0
  21. package/dist/config/rules.d.ts +59 -0
  22. package/dist/config/rules.js +162 -0
  23. package/dist/config/types.d.ts +189 -0
  24. package/dist/config/types.js +2 -0
  25. package/dist/config/validate.d.ts +19 -0
  26. package/dist/config/validate.js +143 -0
  27. package/dist/config/watch.d.ts +53 -0
  28. package/dist/config/watch.js +163 -0
  29. package/dist/fs/module-extensions.d.ts +105 -0
  30. package/dist/fs/module-extensions.js +213 -0
  31. package/dist/head/resolve.d.ts +5 -0
  32. package/dist/head/resolve.js +166 -0
  33. package/dist/head/types.d.ts +44 -0
  34. package/dist/head/types.js +2 -0
  35. package/dist/helpers/html-generator.d.ts +26 -6
  36. package/dist/helpers/html-generator.js +96 -138
  37. package/dist/helpers/middlewares.d.ts +32 -21
  38. package/dist/helpers/middlewares.js +181 -159
  39. package/dist/helpers/page-config-merge.d.ts +11 -0
  40. package/dist/helpers/page-config-merge.js +84 -0
  41. package/dist/helpers/request-handler.d.ts +15 -2
  42. package/dist/helpers/request-handler.js +164 -27
  43. package/dist/index.d.ts +35 -3
  44. package/dist/index.js +77 -8
  45. package/dist/isr/capture.d.ts +79 -0
  46. package/dist/isr/capture.js +222 -0
  47. package/dist/isr/handler.d.ts +49 -0
  48. package/dist/isr/handler.js +207 -0
  49. package/dist/isr/key.d.ts +26 -0
  50. package/dist/isr/key.js +59 -0
  51. package/dist/isr/page-config.d.ts +17 -0
  52. package/dist/isr/page-config.js +71 -0
  53. package/dist/isr/preview.d.ts +5 -0
  54. package/dist/isr/preview.js +38 -0
  55. package/dist/isr/public-request.d.ts +50 -0
  56. package/dist/isr/public-request.js +108 -0
  57. package/dist/isr/response.d.ts +41 -0
  58. package/dist/isr/response.js +128 -0
  59. package/dist/isr/store.d.ts +42 -0
  60. package/dist/isr/store.js +108 -0
  61. package/dist/isr/types.d.ts +62 -0
  62. package/dist/isr/types.js +2 -0
  63. package/dist/pages.d.ts +10 -9
  64. package/dist/pages.js +42 -261
  65. package/dist/platform.d.ts +11 -14
  66. package/dist/platform.js +18 -102
  67. package/dist/plugin/environments.d.ts +68 -0
  68. package/dist/plugin/environments.js +74 -0
  69. package/dist/plugin/index.d.ts +59 -0
  70. package/dist/plugin/index.js +195 -0
  71. package/dist/plugin/route-manifest.d.ts +19 -0
  72. package/dist/plugin/route-manifest.js +55 -0
  73. package/dist/plugin/virtual-entries.d.ts +26 -0
  74. package/dist/plugin/virtual-entries.js +80 -0
  75. package/dist/render.d.ts +39 -2
  76. package/dist/render.js +75 -77
  77. package/dist/router.d.ts +32 -13
  78. package/dist/router.js +180 -146
  79. package/dist/routes/dispatcher.d.ts +58 -0
  80. package/dist/routes/dispatcher.js +70 -0
  81. package/dist/routes/manifest.d.ts +17 -0
  82. package/dist/routes/manifest.js +62 -0
  83. package/dist/routes/match.d.ts +20 -0
  84. package/dist/routes/match.js +75 -0
  85. package/dist/routes/module.d.ts +14 -0
  86. package/dist/routes/module.js +30 -0
  87. package/dist/routes/parse.d.ts +31 -0
  88. package/dist/routes/parse.js +114 -0
  89. package/dist/routes/rank.d.ts +12 -0
  90. package/dist/routes/rank.js +49 -0
  91. package/dist/routes/router.d.ts +54 -0
  92. package/dist/routes/router.js +153 -0
  93. package/dist/routes/scan.d.ts +26 -0
  94. package/dist/routes/scan.js +98 -0
  95. package/dist/routes/types.d.ts +114 -0
  96. package/dist/routes/types.js +17 -0
  97. package/dist/runtime/dev-server.d.ts +64 -0
  98. package/dist/runtime/dev-server.js +94 -0
  99. package/dist/runtime/dev-styles.d.ts +13 -0
  100. package/dist/runtime/dev-styles.js +50 -0
  101. package/dist/runtime/module-loader.d.ts +55 -0
  102. package/dist/runtime/module-loader.js +122 -0
  103. package/dist/runtime/prod-server.d.ts +55 -0
  104. package/dist/runtime/prod-server.js +188 -0
  105. package/dist/runtime/template.d.ts +10 -0
  106. package/dist/runtime/template.js +33 -0
  107. package/dist/security/client-props.d.ts +25 -0
  108. package/dist/security/client-props.js +51 -0
  109. package/dist/security/config.d.ts +121 -0
  110. package/dist/security/config.js +77 -0
  111. package/dist/security/csp.d.ts +53 -0
  112. package/dist/security/csp.js +118 -0
  113. package/dist/security/dev-hardening.d.ts +46 -0
  114. package/dist/security/dev-hardening.js +65 -0
  115. package/dist/security/errors.d.ts +37 -0
  116. package/dist/security/errors.js +84 -0
  117. package/dist/security/escape.d.ts +40 -0
  118. package/dist/security/escape.js +90 -0
  119. package/dist/security/head-tags.d.ts +32 -0
  120. package/dist/security/head-tags.js +156 -0
  121. package/dist/security/headers.d.ts +76 -0
  122. package/dist/security/headers.js +278 -0
  123. package/dist/security/index.d.ts +24 -0
  124. package/dist/security/index.js +49 -0
  125. package/dist/security/serialize.d.ts +48 -0
  126. package/dist/security/serialize.js +146 -0
  127. package/dist/variants/assets.d.ts +33 -0
  128. package/dist/variants/assets.js +144 -0
  129. package/dist/variants/catalog.d.ts +33 -0
  130. package/dist/variants/catalog.js +79 -0
  131. package/dist/variants/platform-tokens.d.ts +9 -0
  132. package/dist/variants/platform-tokens.js +15 -0
  133. package/dist/variants/resolve.d.ts +40 -0
  134. package/dist/variants/resolve.js +90 -0
  135. package/dist/variants/suffix.d.ts +8 -0
  136. package/dist/variants/suffix.js +12 -0
  137. package/dist/variants/types.d.ts +72 -0
  138. package/dist/variants/types.js +2 -0
  139. package/package.json +23 -11
  140. package/dist/helpers/vite-config.d.ts +0 -10
  141. 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;
@@ -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;