@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,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PREVIEW_COOKIE = exports.PREVIEW_HEADER = void 0;
4
+ exports.isPreviewRequest = isPreviewRequest;
5
+ const node_crypto_1 = require("node:crypto");
6
+ exports.PREVIEW_HEADER = 'x-ress-preview';
7
+ exports.PREVIEW_COOKIE = '__ress_preview';
8
+ const digest = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest();
9
+ /** Compara en tiempo constante: el largo del valor tampoco se filtra, porque se comparan hashes. */
10
+ function matches(candidate, secret) {
11
+ if (!candidate)
12
+ return false;
13
+ return (0, node_crypto_1.timingSafeEqual)(digest(candidate), digest(secret));
14
+ }
15
+ function cookieValue(header, name) {
16
+ if (!header)
17
+ return undefined;
18
+ for (const part of header.split(';')) {
19
+ const index = part.indexOf('=');
20
+ if (index === -1 || part.slice(0, index).trim() !== name)
21
+ continue;
22
+ try {
23
+ return decodeURIComponent(part.slice(index + 1).trim());
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+ /** ¿La petición trae el secreto de vista previa? Sin secreto configurado, nunca. */
32
+ function isPreviewRequest(req, secret) {
33
+ if (!secret)
34
+ return false;
35
+ const header = req.headers[exports.PREVIEW_HEADER];
36
+ return (matches(Array.isArray(header) ? header[0] : header, secret) ||
37
+ matches(cookieValue(req.headers.cookie, exports.PREVIEW_COOKIE), secret));
38
+ }
@@ -0,0 +1,50 @@
1
+ import type { Request } from 'express';
2
+ import type { RequestPlatform } from './key';
3
+ /**
4
+ * Las cabeceras que describen a la plataforma, no a la persona.
5
+ *
6
+ * Son las únicas del cliente que una página incremental puede ver: la entrada
7
+ * se separa por plataforma, así que lo que dependa de ellas queda en la entrada
8
+ * correcta. Cualquier otra cabecera podría personalizar un documento que después
9
+ * se sirve a todos.
10
+ */
11
+ export declare function platformHeaderNames(includeUserAgent?: boolean): Set<string>;
12
+ /**
13
+ * El origen de la petición según Express.
14
+ *
15
+ * `req.protocol` y `req.host` respetan `trust proxy`: sin él, un
16
+ * `X-Forwarded-Host` del visitante no cuenta. Es la única vista del origen que
17
+ * llega a la página, y forma parte de la clave: dos hosts nunca comparten
18
+ * entrada, y una cabecera falsificada sólo crea una entrada que nadie más pide.
19
+ */
20
+ export declare function requestOrigin(live: Request): {
21
+ protocol: string;
22
+ host: string;
23
+ };
24
+ export interface PublicRequestInput {
25
+ /** Ruta más la búsqueda canónica. */
26
+ url: string;
27
+ query: Record<string, string | string[]>;
28
+ platform: RequestPlatform;
29
+ /** El de `requestOrigin`: también está en la clave. */
30
+ origin: {
31
+ protocol: string;
32
+ host: string;
33
+ };
34
+ /** Si la detección no usa el User-Agent, la página tampoco lo ve. */
35
+ includeUserAgent?: boolean;
36
+ }
37
+ /** Qué leyó la generación de la petición pública. */
38
+ export declare function publicRequestUsage(req: Request): {
39
+ readsPlatform: boolean;
40
+ };
41
+ /**
42
+ * La petición con la que se genera una página incremental.
43
+ *
44
+ * Es un objeto propio y no una vista de la petición real: heredar de ella
45
+ * dejaría a la generación escribiendo sobre un socket que ya terminó y
46
+ * registrando listeners en otra petición. Lleva sólo lo público —ruta,
47
+ * parámetros declarados y plataforma—, nunca cookies ni credenciales: por
48
+ * construcción, lo que produzca es igual para cualquier visitante.
49
+ */
50
+ export declare function createPublicRequest(live: Request, input: PublicRequestInput): Request;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.platformHeaderNames = platformHeaderNames;
4
+ exports.requestOrigin = requestOrigin;
5
+ exports.publicRequestUsage = publicRequestUsage;
6
+ exports.createPublicRequest = createPublicRequest;
7
+ const node_events_1 = require("node:events");
8
+ const platform_1 = require("@ressjs/platform");
9
+ const platform_tokens_1 = require("../variants/platform-tokens");
10
+ /**
11
+ * Las cabeceras que describen a la plataforma, no a la persona.
12
+ *
13
+ * Son las únicas del cliente que una página incremental puede ver: la entrada
14
+ * se separa por plataforma, así que lo que dependa de ellas queda en la entrada
15
+ * correcta. Cualquier otra cabecera podría personalizar un documento que después
16
+ * se sirve a todos.
17
+ */
18
+ function platformHeaderNames(includeUserAgent = true) {
19
+ return new Set([
20
+ ...platform_1.PLATFORM_VARY_HEADERS.filter((name) => includeUserAgent || name.toLowerCase() !== 'user-agent'),
21
+ platform_1.HEADER_OVERRIDE,
22
+ ...platform_tokens_1.defaultPlatformRegistry.edge.map((rule) => rule.header),
23
+ ].map((name) => name.toLowerCase()));
24
+ }
25
+ /**
26
+ * El origen de la petición según Express.
27
+ *
28
+ * `req.protocol` y `req.host` respetan `trust proxy`: sin él, un
29
+ * `X-Forwarded-Host` del visitante no cuenta. Es la única vista del origen que
30
+ * llega a la página, y forma parte de la clave: dos hosts nunca comparten
31
+ * entrada, y una cabecera falsificada sólo crea una entrada que nadie más pide.
32
+ */
33
+ function requestOrigin(live) {
34
+ const host = live.host ?? String(live.headers?.host ?? 'localhost');
35
+ return { protocol: live.protocol ?? 'http', host: host.toLowerCase() };
36
+ }
37
+ /** Qué leyó la generación de la petición pública. */
38
+ function publicRequestUsage(req) {
39
+ return req.ressUsage ?? { readsPlatform: false };
40
+ }
41
+ /**
42
+ * La petición con la que se genera una página incremental.
43
+ *
44
+ * Es un objeto propio y no una vista de la petición real: heredar de ella
45
+ * dejaría a la generación escribiendo sobre un socket que ya terminó y
46
+ * registrando listeners en otra petición. Lleva sólo lo público —ruta,
47
+ * parámetros declarados y plataforma—, nunca cookies ni credenciales: por
48
+ * construcción, lo que produzca es igual para cualquier visitante.
49
+ */
50
+ function createPublicRequest(live, input) {
51
+ const allowed = platformHeaderNames(input.includeUserAgent ?? true);
52
+ const headers = {};
53
+ for (const [name, value] of Object.entries(live.headers)) {
54
+ if (value !== undefined && allowed.has(name.toLowerCase()))
55
+ headers[name.toLowerCase()] = value;
56
+ }
57
+ // El origen viene de la vista de Express, no de cabeceras crudas: la página
58
+ // no ve `X-Forwarded-*` ni `Accept`, que no forman parte de la clave.
59
+ const { protocol, host } = input.origin;
60
+ headers.host = host;
61
+ const path = input.url.split('?')[0] || '/';
62
+ // Si la página lee una cabecera de plataforma, su HTML puede cambiar según
63
+ // el cliente. ress.js lo separa por plataforma en su caché, pero una CDN no
64
+ // lo sabría: esa página no se le declara compartible. Se registra la lectura
65
+ // en vez de pedirle a la página que lo declare.
66
+ const usage = { readsPlatform: false };
67
+ const tracked = new Proxy(headers, {
68
+ get(target, name) {
69
+ if (typeof name === 'string' && allowed.has(name.toLowerCase()))
70
+ usage.readsPlatform = true;
71
+ return Reflect.get(target, name);
72
+ },
73
+ ownKeys(target) {
74
+ // Recorrerlas todas (un spread, `Object.entries`) también es leerlas.
75
+ if (Object.keys(target).some((name) => allowed.has(name)))
76
+ usage.readsPlatform = true;
77
+ return Reflect.ownKeys(target);
78
+ },
79
+ });
80
+ const header = (name) => tracked[name.toLowerCase()];
81
+ const req = Object.assign(new node_events_1.EventEmitter(), {
82
+ method: 'GET',
83
+ url: input.url,
84
+ originalUrl: input.url,
85
+ baseUrl: '',
86
+ path,
87
+ query: { ...input.query },
88
+ params: { ...(live.ressParams ?? live.params) },
89
+ ressParams: live.ressParams ? { ...live.ressParams } : undefined,
90
+ ressRoute: live.ressRoute,
91
+ ressPlatform: input.platform,
92
+ headers: tracked,
93
+ ressUsage: usage,
94
+ rawHeaders: [],
95
+ httpVersion: '1.1',
96
+ protocol,
97
+ secure: protocol === 'https',
98
+ hostname: host.replace(/:\d+$/, ''),
99
+ host,
100
+ cookies: {},
101
+ signedCookies: {},
102
+ socket: { encrypted: protocol === 'https' },
103
+ connection: { encrypted: protocol === 'https' },
104
+ get: header,
105
+ header,
106
+ });
107
+ return req;
108
+ }
@@ -0,0 +1,41 @@
1
+ import type { Response } from 'express';
2
+ import type { CapturedResponse, HeaderValue, ResolvedPageRendering } from './types';
3
+ export type ServedState = 'MISS' | 'HIT' | 'STALE' | 'BYPASS';
4
+ /**
5
+ * ¿Se puede guardar lo que produjo la generación?
6
+ *
7
+ * `full` es la respuesta completa (para tipo y política de caché, que pueden
8
+ * venir de la semilla); `delta` es lo que agregó la página.
9
+ */
10
+ export declare function cacheability(full: CapturedResponse, delta: Record<string, HeaderValue>): {
11
+ ok: true;
12
+ } | {
13
+ ok: false;
14
+ reason: string;
15
+ };
16
+ /** Guarda una plantilla: el nonce de la política nunca se reutiliza entre respuestas. */
17
+ export declare function normalizeForCache(response: CapturedResponse): CapturedResponse & {
18
+ noncePlaceholder?: string;
19
+ };
20
+ /** Una copia servible de la entrada, con un nonce nuevo si la política lo usa. */
21
+ export declare function materialize(entry: CapturedResponse & {
22
+ noncePlaceholder?: string;
23
+ }): CapturedResponse;
24
+ /**
25
+ * Lo que una página incremental le dice a una CDN.
26
+ *
27
+ * Con nonce no se declara compartible: una CDN serviría el mismo nonce a todos,
28
+ * que es exactamente lo que el nonce existe para impedir. Sin plazo tampoco:
29
+ * la entrada vence con el reinicio del proceso y una CDN no se enteraría.
30
+ */
31
+ export declare function isrCacheControl(rendering: ResolvedPageRendering, usesNonce: boolean): string | undefined;
32
+ /**
33
+ * Envía lo capturado por la respuesta real.
34
+ *
35
+ * Los middlewares ya pusieron lo suyo en esta petición; lo capturado va encima,
36
+ * como en el camino normal. `res.send` calcula longitud y ETag, y la compresión
37
+ * actúa sobre esta respuesta según lo que acepte este cliente.
38
+ */
39
+ /** Lo mismo que el resto del framework: cualquier directiva que deje guardarla a una CDN. */
40
+ export declare function isSharedCacheControl(value: HeaderValue | undefined): boolean;
41
+ export declare function applyCaptured(res: Response, response: CapturedResponse, state: ServedState): void;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cacheability = cacheability;
4
+ exports.normalizeForCache = normalizeForCache;
5
+ exports.materialize = materialize;
6
+ exports.isrCacheControl = isrCacheControl;
7
+ exports.isSharedCacheControl = isSharedCacheControl;
8
+ exports.applyCaptured = applyCaptured;
9
+ const security_1 = require("../security");
10
+ const capture_1 = require("./capture");
11
+ const public_request_1 = require("./public-request");
12
+ const NONCE_PLACEHOLDER = '__RESS_ISR_NONCE__';
13
+ const ONE_YEAR = 31_536_000;
14
+ /**
15
+ * `Vary` que una entrada puede declarar sin quedar mal identificada: la clave ya
16
+ * separa por plataforma, y la codificación se negocia en cada petición.
17
+ */
18
+ let safeVary;
19
+ function isSafeVary(field) {
20
+ safeVary ??= new Set([...(0, public_request_1.platformHeaderNames)(), 'accept-encoding']);
21
+ return safeVary.has(field.toLowerCase());
22
+ }
23
+ /**
24
+ * ¿Se puede guardar lo que produjo la generación?
25
+ *
26
+ * `full` es la respuesta completa (para tipo y política de caché, que pueden
27
+ * venir de la semilla); `delta` es lo que agregó la página.
28
+ */
29
+ function cacheability(full, delta) {
30
+ if (full.status !== 200)
31
+ return { ok: false, reason: `respondió ${full.status}` };
32
+ if (!String(full.headers['content-type'] ?? '').toLowerCase().includes('text/html')) {
33
+ return { ok: false, reason: 'no respondió HTML' };
34
+ }
35
+ if (delta['set-cookie'] !== undefined)
36
+ return { ok: false, reason: 'entregó una cookie' };
37
+ if (/\bno-store\b/i.test(String(full.headers['cache-control'] ?? ''))) {
38
+ return { ok: false, reason: 'declaró Cache-Control: no-store' };
39
+ }
40
+ const foreign = (0, capture_1.varyFields)(delta.vary).filter((field) => !isSafeVary(field));
41
+ if (foreign.length) {
42
+ return { ok: false, reason: `varía por ${foreign.join(', ')}, que no forma parte de la clave` };
43
+ }
44
+ return { ok: true };
45
+ }
46
+ const mapHeaders = (headers, fn) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value.map(fn) : fn(value)]));
47
+ /** Guarda una plantilla: el nonce de la política nunca se reutiliza entre respuestas. */
48
+ function normalizeForCache(response) {
49
+ const csp = response.headers['content-security-policy'] ?? response.headers['content-security-policy-report-only'];
50
+ const nonce = (Array.isArray(csp) ? csp.join(';') : csp)?.match(/'nonce-([^']+)'/)?.[1];
51
+ if (!nonce)
52
+ return response;
53
+ const replace = (value) => value.replaceAll(nonce, NONCE_PLACEHOLDER);
54
+ return {
55
+ ...response,
56
+ body: replace(response.body),
57
+ headers: mapHeaders(response.headers, replace),
58
+ noncePlaceholder: NONCE_PLACEHOLDER,
59
+ };
60
+ }
61
+ /** Una copia servible de la entrada, con un nonce nuevo si la política lo usa. */
62
+ function materialize(entry) {
63
+ const placeholder = entry.noncePlaceholder;
64
+ if (!placeholder)
65
+ return entry;
66
+ const nonce = (0, security_1.createNonce)();
67
+ const replace = (value) => value.replaceAll(placeholder, nonce);
68
+ return { status: entry.status, body: replace(entry.body), headers: mapHeaders(entry.headers, replace) };
69
+ }
70
+ /**
71
+ * Lo que una página incremental le dice a una CDN.
72
+ *
73
+ * Con nonce no se declara compartible: una CDN serviría el mismo nonce a todos,
74
+ * que es exactamente lo que el nonce existe para impedir. Sin plazo tampoco:
75
+ * la entrada vence con el reinicio del proceso y una CDN no se enteraría.
76
+ */
77
+ function isrCacheControl(rendering, usesNonce) {
78
+ if (usesNonce || rendering.revalidate === false)
79
+ return undefined;
80
+ const swr = rendering.expire !== undefined ? rendering.expire - rendering.revalidate : ONE_YEAR;
81
+ return `public, max-age=0, s-maxage=${rendering.revalidate}, stale-while-revalidate=${swr}`;
82
+ }
83
+ /**
84
+ * Envía lo capturado por la respuesta real.
85
+ *
86
+ * Los middlewares ya pusieron lo suyo en esta petición; lo capturado va encima,
87
+ * como en el camino normal. `res.send` calcula longitud y ETag, y la compresión
88
+ * actúa sobre esta respuesta según lo que acepte este cliente.
89
+ */
90
+ /** Lo mismo que el resto del framework: cualquier directiva que deje guardarla a una CDN. */
91
+ function isSharedCacheControl(value) {
92
+ return (0, security_1.isShareableCacheControl)(Array.isArray(value) ? value.join(',') : value);
93
+ }
94
+ /**
95
+ * Una respuesta que entrega una cookie nunca se declara compartible.
96
+ *
97
+ * La generación no ve las cookies que ponen los middlewares —se sacan de la
98
+ * semilla para detectar las de la página—, así que la comprobación va sobre la
99
+ * respuesta real y lo más tarde posible: al escribir las cabeceras, cuando ya
100
+ * pasó cualquiera que agregue la suya en ese momento (una sesión, por ejemplo).
101
+ */
102
+ function forbidSharedCacheWithCookies(res) {
103
+ const writeHead = res.writeHead.bind(res);
104
+ res.writeHead = ((...args) => {
105
+ if (res.getHeader('Set-Cookie') !== undefined && isSharedCacheControl(res.getHeader('Cache-Control'))) {
106
+ res.setHeader('Cache-Control', 'private, no-cache');
107
+ }
108
+ return writeHead(...args);
109
+ });
110
+ }
111
+ function applyCaptured(res, response, state) {
112
+ forbidSharedCacheWithCookies(res);
113
+ for (const [name, value] of Object.entries(response.headers)) {
114
+ if (name === 'vary') {
115
+ for (const field of (0, capture_1.varyFields)(value))
116
+ res.vary(field);
117
+ }
118
+ else if (name === 'set-cookie') {
119
+ res.append('Set-Cookie', value);
120
+ }
121
+ else {
122
+ res.setHeader(name, value);
123
+ }
124
+ }
125
+ res.status(response.status);
126
+ res.setHeader('X-Ress-Cache', state);
127
+ res.send(response.body);
128
+ }
@@ -0,0 +1,42 @@
1
+ import type { CacheLookup, IsrEntry } from './types';
2
+ export interface MemoryIsrStoreOptions {
3
+ maxEntries: number;
4
+ maxBytes: number;
5
+ /** Tiempo máximo de una generación compartida. */
6
+ timeoutMs: number;
7
+ }
8
+ export declare class GenerationTimeoutError extends Error {
9
+ constructor(key: string, timeoutMs: number);
10
+ }
11
+ /**
12
+ * La caché incremental de un proceso.
13
+ *
14
+ * LRU por cantidad y por bytes: con rutas dinámicas el conjunto de URLs es
15
+ * abierto, y un tope sólo por cantidad deja crecer la memoria con páginas grandes.
16
+ * Cada router crea la suya, así que dos aplicaciones en el mismo proceso no
17
+ * comparten documentos.
18
+ */
19
+ export declare class MemoryIsrStore {
20
+ #private;
21
+ readonly options: MemoryIsrStoreOptions;
22
+ constructor(options: MemoryIsrStoreOptions);
23
+ lookup(key: string, now?: number): CacheLookup;
24
+ set(entry: IsrEntry): void;
25
+ delete(key: string): void;
26
+ /**
27
+ * Una sola generación por clave a la vez.
28
+ *
29
+ * Quien llega mientras hay una en curso recibe la misma promesa: una avalancha
30
+ * sobre una entrada nueva o vencida renderiza una vez. Si vence el tiempo, la
31
+ * clave se libera para que la próxima petición pueda intentarlo; lo que la
32
+ * generación abandonada produzca después no lo consume nadie, así que nunca
33
+ * llega a guardarse.
34
+ */
35
+ run<T>(key: string, work: () => Promise<T>): Promise<T>;
36
+ inFlight(key: string): boolean;
37
+ clear(): void;
38
+ get size(): number;
39
+ get bytes(): number;
40
+ }
41
+ /** La clave sin el build ni la plataforma completa, para los mensajes. */
42
+ export declare function readableKey(key: string): string;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryIsrStore = exports.GenerationTimeoutError = void 0;
4
+ exports.readableKey = readableKey;
5
+ class GenerationTimeoutError extends Error {
6
+ constructor(key, timeoutMs) {
7
+ super(`[ress] La generación ISR excedió ${timeoutMs} ms y se abandonó (${readableKey(key)}).`);
8
+ this.name = 'GenerationTimeoutError';
9
+ }
10
+ }
11
+ exports.GenerationTimeoutError = GenerationTimeoutError;
12
+ /**
13
+ * La caché incremental de un proceso.
14
+ *
15
+ * LRU por cantidad y por bytes: con rutas dinámicas el conjunto de URLs es
16
+ * abierto, y un tope sólo por cantidad deja crecer la memoria con páginas grandes.
17
+ * Cada router crea la suya, así que dos aplicaciones en el mismo proceso no
18
+ * comparten documentos.
19
+ */
20
+ class MemoryIsrStore {
21
+ options;
22
+ #entries = new Map();
23
+ #inFlight = new Map();
24
+ #bytes = 0;
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ lookup(key, now = Date.now()) {
29
+ const entry = this.#entries.get(key);
30
+ if (!entry)
31
+ return { state: 'missing' };
32
+ // Leer también es usar: se mueve al final del orden de desalojo.
33
+ this.#entries.delete(key);
34
+ this.#entries.set(key, entry);
35
+ if (entry.revalidateMs === null)
36
+ return { state: 'fresh', entry };
37
+ const age = now - entry.generatedAt;
38
+ if (age < entry.revalidateMs)
39
+ return { state: 'fresh', entry };
40
+ if (entry.expireMs !== null && age >= entry.expireMs)
41
+ return { state: 'expired', entry };
42
+ return { state: 'stale', entry };
43
+ }
44
+ set(entry) {
45
+ this.delete(entry.key);
46
+ // Una entrada que no entra sola desalojaría toda la caché para no caber igual.
47
+ if (entry.bytes > this.options.maxBytes)
48
+ return;
49
+ this.#entries.set(entry.key, entry);
50
+ this.#bytes += entry.bytes;
51
+ for (const oldest of this.#entries.keys()) {
52
+ if (this.#entries.size <= this.options.maxEntries && this.#bytes <= this.options.maxBytes)
53
+ break;
54
+ this.delete(oldest);
55
+ }
56
+ }
57
+ delete(key) {
58
+ const previous = this.#entries.get(key);
59
+ if (!previous)
60
+ return;
61
+ this.#entries.delete(key);
62
+ this.#bytes -= previous.bytes;
63
+ }
64
+ /**
65
+ * Una sola generación por clave a la vez.
66
+ *
67
+ * Quien llega mientras hay una en curso recibe la misma promesa: una avalancha
68
+ * sobre una entrada nueva o vencida renderiza una vez. Si vence el tiempo, la
69
+ * clave se libera para que la próxima petición pueda intentarlo; lo que la
70
+ * generación abandonada produzca después no lo consume nadie, así que nunca
71
+ * llega a guardarse.
72
+ */
73
+ run(key, work) {
74
+ const running = this.#inFlight.get(key);
75
+ if (running)
76
+ return running;
77
+ let timer;
78
+ const timeout = new Promise((_, reject) => {
79
+ timer = setTimeout(() => reject(new GenerationTimeoutError(key, this.options.timeoutMs)), this.options.timeoutMs);
80
+ timer.unref?.();
81
+ });
82
+ const task = Promise.race([Promise.resolve().then(work), timeout]).finally(() => {
83
+ clearTimeout(timer);
84
+ if (this.#inFlight.get(key) === task)
85
+ this.#inFlight.delete(key);
86
+ });
87
+ this.#inFlight.set(key, task);
88
+ return task;
89
+ }
90
+ inFlight(key) {
91
+ return this.#inFlight.has(key);
92
+ }
93
+ clear() {
94
+ this.#entries.clear();
95
+ this.#bytes = 0;
96
+ }
97
+ get size() {
98
+ return this.#entries.size;
99
+ }
100
+ get bytes() {
101
+ return this.#bytes;
102
+ }
103
+ }
104
+ exports.MemoryIsrStore = MemoryIsrStore;
105
+ /** La clave sin el build ni la plataforma completa, para los mensajes. */
106
+ function readableKey(key) {
107
+ return key.split('\0').at(-1) ?? key;
108
+ }
@@ -0,0 +1,62 @@
1
+ export type RenderingMode = 'ssr' | 'isr';
2
+ /**
3
+ * Cómo se renderiza una página. Vive en el export estático `pageConfig`: el
4
+ * router tiene que decidir si responde desde la caché antes de ejecutar nada de
5
+ * la petición, así que no puede depender de lo que devuelva `getServerSideProps`.
6
+ */
7
+ export interface PageRenderingConfig {
8
+ /** `ssr` (por defecto) renderiza cada petición; `isr` sirve desde la caché. */
9
+ renderingMode?: RenderingMode;
10
+ /**
11
+ * Segundos durante los que una entrada es fresca (entero mayor que cero), o
12
+ * `false`: no vence por tiempo, sólo con un build nuevo. Por defecto `false`.
13
+ */
14
+ revalidate?: number | false;
15
+ /**
16
+ * Techo de antigüedad en segundos. Pasado, la entrada ya no se sirve vencida:
17
+ * se regenera antes de responder. Requiere un `revalidate` numérico menor.
18
+ */
19
+ expire?: number;
20
+ /** `false` entrega HTML y CSS sin script de entrada ni estado. Por defecto `true`. */
21
+ hydrate?: boolean;
22
+ /** Parámetros de consulta que forman parte de la identidad de la página. */
23
+ searchParams?: string[];
24
+ }
25
+ /** La configuración de render ya validada, en las unidades que usa el runtime. */
26
+ export interface ResolvedPageRendering {
27
+ mode: RenderingMode;
28
+ hydrate: boolean;
29
+ /** `null`: no vence por tiempo. */
30
+ revalidateMs: number | null;
31
+ /** `null`: sin techo. */
32
+ expireMs: number | null;
33
+ /** Únicos y ordenados. */
34
+ searchParams: string[];
35
+ /** En segundos, para `Cache-Control`. */
36
+ revalidate: number | false;
37
+ expire?: number;
38
+ }
39
+ export type HeaderValue = string | string[];
40
+ /** Lo que produjo una generación: sólo lo que agregó la página, no el transporte. */
41
+ export interface CapturedResponse {
42
+ status: number;
43
+ /** En minúsculas. */
44
+ headers: Record<string, HeaderValue>;
45
+ body: string;
46
+ }
47
+ export interface IsrEntry extends CapturedResponse {
48
+ key: string;
49
+ generatedAt: number;
50
+ revalidateMs: number | null;
51
+ expireMs: number | null;
52
+ /** Marcador que reemplazó al nonce original, si la respuesta tenía uno. */
53
+ noncePlaceholder?: string;
54
+ bytes: number;
55
+ }
56
+ export type CacheState = 'fresh' | 'stale' | 'expired' | 'missing';
57
+ export type CacheLookup = {
58
+ state: 'missing';
59
+ } | {
60
+ state: Exclude<CacheState, 'missing'>;
61
+ entry: IsrEntry;
62
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/pages.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { PlatformInfo } from './platform';
1
+ import { type ExtensionPolicy } from './fs/module-extensions';
2
2
  export interface PageInfo {
3
3
  route: string;
4
4
  expressRoute: string;
5
5
  file: string;
6
6
  abs: string;
7
+ /** Identificador estable, compartido con el catálogo de variantes. */
8
+ pageId: string;
7
9
  }
8
10
  /**
9
11
  * Convierte rutas estilo Next.js con parámetros dinámicos a formato Express
@@ -13,18 +15,17 @@ export interface PageInfo {
13
15
  * '/home' → '/home' (sin cambios)
14
16
  */
15
17
  export declare function convertRouteToExpress(route: string): string;
16
- export declare function getPages(): PageInfo[];
17
- export declare function getViteInput(): Record<string, string>;
18
+ export declare function getPages(policy?: ExtensionPolicy): PageInfo[];
19
+ /**
20
+ * Entradas del entorno de servidor: las páginas más sus middlewares.
21
+ *
22
+ * El entorno de cliente no aparece acá: sus entradas salen del catálogo de
23
+ * variantes, porque una página produce una por cada plataforma que atiende.
24
+ */
18
25
  export declare function getViteSSRInput(): Record<string, string>;
19
- export declare function loadManifest(distPath?: string): Promise<any>;
20
26
  /**
21
27
  * Convierte una ruta de página TypeScript a su equivalente compilado en producción
22
28
  *
23
29
  * @param pageFile - Ruta del archivo de página (relativa: "app/pages/pokedex/index.tsx" o absoluta: "/path/to/app/pages/pokedex/index.tsx")
24
30
  * @returns Ruta absoluta del archivo compilado (ej: "/path/to/project/dist/server/pokedex/index.js")
25
31
  */
26
- export declare function getCompiledProdPage(pageFile: string): string;
27
- export declare function resolvePlatformCSS(basePath: string, platform: PlatformInfo): Promise<string>;
28
- export declare function generateTempEntries(distPath?: string, platformInfo?: PlatformInfo): Promise<string[]>;
29
- export declare function generateSingleEntry(page: PageInfo, entryPath: string, preferredScssFile: string): Promise<void>;
30
- export declare function getClientEntriesInput(distPath?: string): Promise<Record<string, string>>;