@ressjs/vite-router 0.5.2 → 0.6.0-experimental.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/README.md +42 -32
- 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 +23 -0
- package/dist/config/codegen.js +118 -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 +44 -0
- package/dist/config/load.d.ts +26 -0
- package/dist/config/load.js +73 -0
- package/dist/config/middleware.d.ts +18 -0
- package/dist/config/middleware.js +79 -0
- package/dist/config/rules.d.ts +48 -0
- package/dist/config/rules.js +149 -0
- package/dist/config/types.d.ts +164 -0
- package/dist/config/types.js +2 -0
- package/dist/config/validate.d.ts +19 -0
- package/dist/config/validate.js +105 -0
- package/dist/config/watch.d.ts +41 -0
- package/dist/config/watch.js +132 -0
- package/dist/fs/module-extensions.d.ts +82 -0
- package/dist/fs/module-extensions.js +196 -0
- package/dist/helpers/html-generator.d.ts +22 -5
- package/dist/helpers/html-generator.js +89 -120
- package/dist/helpers/middlewares.d.ts +7 -7
- package/dist/helpers/middlewares.js +51 -105
- package/dist/helpers/request-handler.d.ts +2 -2
- package/dist/helpers/request-handler.js +118 -25
- package/dist/index.d.ts +30 -3
- package/dist/index.js +70 -8
- 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 +66 -0
- package/dist/plugin/environments.js +72 -0
- package/dist/plugin/index.d.ts +54 -0
- package/dist/plugin/index.js +178 -0
- package/dist/plugin/virtual-entries.d.ts +26 -0
- package/dist/plugin/virtual-entries.js +80 -0
- package/dist/render.d.ts +23 -2
- package/dist/render.js +40 -53
- package/dist/router.d.ts +32 -13
- package/dist/router.js +147 -146
- package/dist/routes/dispatcher.d.ts +37 -0
- package/dist/routes/dispatcher.js +69 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +56 -0
- package/dist/routes/match.d.ts +20 -0
- package/dist/routes/match.js +75 -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 +44 -0
- package/dist/routes/router.js +81 -0
- package/dist/routes/scan.d.ts +16 -0
- package/dist/routes/scan.js +82 -0
- package/dist/routes/types.d.ts +85 -0
- package/dist/routes/types.js +13 -0
- package/dist/runtime/dev-server.d.ts +62 -0
- package/dist/runtime/dev-server.js +94 -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 +42 -0
- package/dist/runtime/prod-server.js +142 -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 +50 -0
- package/dist/security/config.d.ts +130 -0
- package/dist/security/config.js +73 -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 +111 -0
- package/dist/security/headers.d.ts +62 -0
- package/dist/security/headers.js +187 -0
- package/dist/security/index.d.ts +24 -0
- package/dist/security/index.js +47 -0
- package/dist/security/serialize.d.ts +48 -0
- package/dist/security/serialize.js +146 -0
- package/dist/variants/assets.d.ts +23 -0
- package/dist/variants/assets.js +78 -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 +69 -0
- package/dist/variants/types.js +2 -0
- package/package.json +22 -10
- package/dist/helpers/vite-config.d.ts +0 -10
- package/dist/helpers/vite-config.js +0 -90
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Qué puede poner una página en el `<head>`.
|
|
4
|
+
*
|
|
5
|
+
* El `<head>` es el lugar más rentable para inyectar algo: un `<script>` ahí
|
|
6
|
+
* corre antes que la página, un `<meta http-equiv="refresh">` la redirige, y un
|
|
7
|
+
* `<base>` cambia a dónde apunta cada URL relativa del documento. Por eso lo que
|
|
8
|
+
* se admite es una lista cerrada y no una lista de lo prohibido.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.HEAD_TAG_ALLOWLIST = void 0;
|
|
12
|
+
exports.renderHeadTags = renderHeadTags;
|
|
13
|
+
const escape_1 = require("./escape");
|
|
14
|
+
exports.HEAD_TAG_ALLOWLIST = ['meta', 'link', 'title', 'base', 'style'];
|
|
15
|
+
/** Etiquetas sin contenido, que se emiten autocerradas. */
|
|
16
|
+
const VOID_TAGS = new Set(['meta', 'link', 'base']);
|
|
17
|
+
/**
|
|
18
|
+
* Valores de `rel` admitidos en un `<link>`.
|
|
19
|
+
*
|
|
20
|
+
* Fuera de esta lista quedan cosas como `import`, que carga y ejecuta un
|
|
21
|
+
* documento entero.
|
|
22
|
+
*/
|
|
23
|
+
const LINK_REL_ALLOWLIST = new Set([
|
|
24
|
+
'stylesheet',
|
|
25
|
+
'preload',
|
|
26
|
+
'prefetch',
|
|
27
|
+
'preconnect',
|
|
28
|
+
'dns-prefetch',
|
|
29
|
+
'icon',
|
|
30
|
+
'apple-touch-icon',
|
|
31
|
+
'manifest',
|
|
32
|
+
'canonical',
|
|
33
|
+
'alternate',
|
|
34
|
+
'modulepreload',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* `http-equiv` que se descartan: uno redirige la página y el otro declara una
|
|
38
|
+
* política de contenido que debilitaría la que emite el framework por cabecera.
|
|
39
|
+
*/
|
|
40
|
+
const BLOCKED_HTTP_EQUIV = new Set(['refresh', 'content-security-policy']);
|
|
41
|
+
/**
|
|
42
|
+
* Filtra y emite las etiquetas adicionales del `<head>`.
|
|
43
|
+
*
|
|
44
|
+
* Nunca se concatena un string de HTML crudo que aporte la aplicación: la
|
|
45
|
+
* entrada es siempre estructurada, y cada atributo pasa por el escapado.
|
|
46
|
+
*/
|
|
47
|
+
function renderHeadTags(tags, ctx, options = {}) {
|
|
48
|
+
const allowed = new Set([
|
|
49
|
+
...exports.HEAD_TAG_ALLOWLIST,
|
|
50
|
+
...(options.extraAllowlist ?? []),
|
|
51
|
+
...(options.allowScriptTags ? ['script'] : []),
|
|
52
|
+
]);
|
|
53
|
+
const out = [];
|
|
54
|
+
let baseEmitted = false;
|
|
55
|
+
for (const tag of tags ?? []) {
|
|
56
|
+
const name = String(tag?.tag ?? '').toLowerCase();
|
|
57
|
+
if (!allowed.has(name)) {
|
|
58
|
+
discard(ctx, name, 'no está entre las etiquetas admitidas en <head>');
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (name === 'base') {
|
|
62
|
+
if (baseEmitted) {
|
|
63
|
+
discard(ctx, name, 'ya hay un <base>; sólo se admite uno por documento');
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
baseEmitted = true;
|
|
67
|
+
}
|
|
68
|
+
if (name === 'link' && !isAllowedLink(tag)) {
|
|
69
|
+
discard(ctx, name, `su rel no está admitido`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (name === 'meta' && isBlockedMeta(tag)) {
|
|
73
|
+
discard(ctx, name, 'su http-equiv no está admitido');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// El contenido de un <style> se emite sin escapar —es CSS, no HTML—, así
|
|
77
|
+
// que lo único que puede sacarlo de su elemento es un cierre literal.
|
|
78
|
+
if (name === 'style' && /<\/style/i.test(tag.children ?? '')) {
|
|
79
|
+
discard(ctx, name, 'su contenido cierra el elemento');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
out.push(emit(name, tag, options.nonce));
|
|
83
|
+
}
|
|
84
|
+
return out.join('\n');
|
|
85
|
+
}
|
|
86
|
+
function emit(name, tag, nonce) {
|
|
87
|
+
const attrs = { ...(tag.attrs ?? {}) };
|
|
88
|
+
if (nonce && (name === 'style' || name === 'script'))
|
|
89
|
+
attrs.nonce = nonce;
|
|
90
|
+
const rendered = (0, escape_1.renderAttrs)(attrs, { where: `<${name}>`, isProduction: true });
|
|
91
|
+
const open = rendered ? `<${name} ${rendered}` : `<${name}`;
|
|
92
|
+
if (VOID_TAGS.has(name))
|
|
93
|
+
return `${open}>`;
|
|
94
|
+
// El CSS de un <style> se emite tal cual; todo lo demás es texto y se escapa.
|
|
95
|
+
const body = name === 'style' ? (tag.children ?? '') : (0, escape_1.escapeHtmlText)(tag.children ?? '');
|
|
96
|
+
return `${open}>${body}</${name}>`;
|
|
97
|
+
}
|
|
98
|
+
function isAllowedLink(tag) {
|
|
99
|
+
const rel = String(tag.attrs?.rel ?? '').toLowerCase().trim();
|
|
100
|
+
return rel !== '' && rel.split(/\s+/).every((r) => LINK_REL_ALLOWLIST.has(r));
|
|
101
|
+
}
|
|
102
|
+
function isBlockedMeta(tag) {
|
|
103
|
+
const httpEquiv = String(tag.attrs?.['http-equiv'] ?? '').toLowerCase().trim();
|
|
104
|
+
return BLOCKED_HTTP_EQUIV.has(httpEquiv);
|
|
105
|
+
}
|
|
106
|
+
function discard(ctx, name, why) {
|
|
107
|
+
if (ctx.isProduction)
|
|
108
|
+
return;
|
|
109
|
+
const where = ctx.route ? ` de ${ctx.route}` : '';
|
|
110
|
+
console.warn(`[ress] etiqueta <${name}> descartada en <head>${where}: ${why}.`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cabeceras de la respuesta.
|
|
3
|
+
*
|
|
4
|
+
* Todas se emiten **antes** de escribir el cuerpo: una vez que la respuesta
|
|
5
|
+
* empezó a escribirse ya no se pueden cambiar, y el intento tira una excepción
|
|
6
|
+
* que enmascara el error original.
|
|
7
|
+
*/
|
|
8
|
+
import type { ServerResponse } from 'node:http';
|
|
9
|
+
import { type Axis } from '@ressjs/platform';
|
|
10
|
+
import type { ResolvedSecurityOptions } from './config';
|
|
11
|
+
export interface HeaderContext {
|
|
12
|
+
isProduction: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Ejes por los que **esta** respuesta varía.
|
|
15
|
+
*
|
|
16
|
+
* Sale de las variantes que la página declara: una página sin variantes
|
|
17
|
+
* devuelve el mismo documento a cualquier cliente, y declarar que varía por
|
|
18
|
+
* dieciocho cabeceras obliga a una caché intermedia a guardar una entrada por
|
|
19
|
+
* combinación de algo que no cambia nunca.
|
|
20
|
+
*
|
|
21
|
+
* Vacío o `null` significa que no se emite `Vary` de plataforma.
|
|
22
|
+
*/
|
|
23
|
+
varyBy?: readonly Axis[] | null;
|
|
24
|
+
/** Cabeceras de la red de distribución que el proyecto declaró. */
|
|
25
|
+
edge?: readonly {
|
|
26
|
+
header: string;
|
|
27
|
+
axis: string;
|
|
28
|
+
}[];
|
|
29
|
+
nonce?: string;
|
|
30
|
+
kind: 'html' | 'error';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Agrega campos a `Vary` sin duplicar y sin reemplazar lo que ya haya.
|
|
34
|
+
*
|
|
35
|
+
* Reemplazar sería destruir el trabajo de un middleware anterior: si alguien
|
|
36
|
+
* declaró `Vary: Accept-Language` porque su respuesta depende del idioma, y el
|
|
37
|
+
* framework lo pisa, el CDN sirve la traducción equivocada.
|
|
38
|
+
*
|
|
39
|
+
* Un `Vary: *` existente no se toca: ya es más restrictivo que cualquier lista.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendVary(res: ServerResponse, fields: readonly string[]): void;
|
|
42
|
+
/**
|
|
43
|
+
* Aplica las cabeceras de seguridad de una respuesta.
|
|
44
|
+
*
|
|
45
|
+
* Es idempotente y no pisa lo que la aplicación ya haya definido, con dos
|
|
46
|
+
* excepciones: `Vary`, que amplía, y el `Content-Type` del HTML, que fija —el
|
|
47
|
+
* `charset` explícito es parte del endurecimiento, porque sin él un navegador
|
|
48
|
+
* puede inferir una codificación en la que los mismos bytes significan otra cosa.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applySecurityHeaders(res: ServerResponse, opts: ResolvedSecurityOptions, ctx: HeaderContext): void;
|
|
51
|
+
/**
|
|
52
|
+
* Cuánto puede cachearse un archivo estático.
|
|
53
|
+
*
|
|
54
|
+
* Un nombre con hash de contenido cambia cuando cambia el contenido, así que se
|
|
55
|
+
* puede guardar para siempre. Cualquier otro nombre puede volver a servirse con
|
|
56
|
+
* contenido distinto, y un archivo guardado como inmutable por error es
|
|
57
|
+
* irrecuperable: no hay forma de invalidarlo en el navegador de quien ya lo
|
|
58
|
+
* bajó. Por eso el default es revalidar.
|
|
59
|
+
*/
|
|
60
|
+
export declare function classifyAsset(pathname: string, buildOutputs: ReadonlySet<string>): 'immutable' | 'revalidate';
|
|
61
|
+
/** El valor de `Cache-Control` de un archivo estático según su clase. */
|
|
62
|
+
export declare function assetCacheControl(kind: 'immutable' | 'revalidate', opts: ResolvedSecurityOptions): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cabeceras de la respuesta.
|
|
4
|
+
*
|
|
5
|
+
* Todas se emiten **antes** de escribir el cuerpo: una vez que la respuesta
|
|
6
|
+
* empezó a escribirse ya no se pueden cambiar, y el intento tira una excepción
|
|
7
|
+
* que enmascara el error original.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.appendVary = appendVary;
|
|
11
|
+
exports.applySecurityHeaders = applySecurityHeaders;
|
|
12
|
+
exports.classifyAsset = classifyAsset;
|
|
13
|
+
exports.assetCacheControl = assetCacheControl;
|
|
14
|
+
const platform_1 = require("@ressjs/platform");
|
|
15
|
+
/**
|
|
16
|
+
* Agrega campos a `Vary` sin duplicar y sin reemplazar lo que ya haya.
|
|
17
|
+
*
|
|
18
|
+
* Reemplazar sería destruir el trabajo de un middleware anterior: si alguien
|
|
19
|
+
* declaró `Vary: Accept-Language` porque su respuesta depende del idioma, y el
|
|
20
|
+
* framework lo pisa, el CDN sirve la traducción equivocada.
|
|
21
|
+
*
|
|
22
|
+
* Un `Vary: *` existente no se toca: ya es más restrictivo que cualquier lista.
|
|
23
|
+
*/
|
|
24
|
+
function appendVary(res, fields) {
|
|
25
|
+
const current = res.getHeader('Vary');
|
|
26
|
+
const existing = Array.isArray(current) ? current.join(', ') : String(current ?? '');
|
|
27
|
+
if (existing.trim() === '*')
|
|
28
|
+
return;
|
|
29
|
+
const present = new Set(existing
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((f) => f.trim().toLowerCase())
|
|
32
|
+
.filter(Boolean));
|
|
33
|
+
const merged = existing ? [existing.trim()] : [];
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
if (present.has(field.toLowerCase()))
|
|
36
|
+
continue;
|
|
37
|
+
present.add(field.toLowerCase());
|
|
38
|
+
merged.push(field);
|
|
39
|
+
}
|
|
40
|
+
if (merged.length)
|
|
41
|
+
res.setHeader('Vary', merged.join(', '));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Aplica las cabeceras de seguridad de una respuesta.
|
|
45
|
+
*
|
|
46
|
+
* Es idempotente y no pisa lo que la aplicación ya haya definido, con dos
|
|
47
|
+
* excepciones: `Vary`, que amplía, y el `Content-Type` del HTML, que fija —el
|
|
48
|
+
* `charset` explícito es parte del endurecimiento, porque sin él un navegador
|
|
49
|
+
* puede inferir una codificación en la que los mismos bytes significan otra cosa.
|
|
50
|
+
*/
|
|
51
|
+
function applySecurityHeaders(res, opts, ctx) {
|
|
52
|
+
if (res.headersSent) {
|
|
53
|
+
if (!ctx.isProduction) {
|
|
54
|
+
console.warn('[ress] no se pudieron aplicar las cabeceras de seguridad: la respuesta ya empezó a enviarse.');
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const fields = (0, platform_1.varyHeadersForAxes)(ctx.varyBy ?? [], {
|
|
59
|
+
includeUserAgent: opts.varyOnUserAgent,
|
|
60
|
+
// Sólo las que el proyecto declaró para su red: declarar cabeceras que nadie
|
|
61
|
+
// manda vuelve a inflar la clave de caché, que es lo que se quería evitar.
|
|
62
|
+
edge: ctx.edge,
|
|
63
|
+
});
|
|
64
|
+
if (fields.length) {
|
|
65
|
+
appendVary(res, fields);
|
|
66
|
+
requestClientHints(res);
|
|
67
|
+
}
|
|
68
|
+
if (opts.headers.contentTypeOptions)
|
|
69
|
+
setIfAbsent(res, 'X-Content-Type-Options', 'nosniff');
|
|
70
|
+
if (opts.headers.referrerPolicy) {
|
|
71
|
+
setIfAbsent(res, 'Referrer-Policy', opts.headers.referrerPolicy);
|
|
72
|
+
}
|
|
73
|
+
// No se emite por defecto: ress.js sirve páginas embebidas en apps por diseño,
|
|
74
|
+
// y una política de encuadre rompería el caso de uso principal.
|
|
75
|
+
if (opts.headers.frameOptions) {
|
|
76
|
+
setIfAbsent(res, 'X-Frame-Options', opts.headers.frameOptions);
|
|
77
|
+
}
|
|
78
|
+
// Apagada por defecto: activarla sobre un dominio que todavía sirve HTTP por
|
|
79
|
+
// alguna ruta lo deja inaccesible, y esa decisión es del proyecto.
|
|
80
|
+
if (opts.headers.hsts && ctx.isProduction) {
|
|
81
|
+
const { maxAge, includeSubDomains, preload } = opts.headers.hsts;
|
|
82
|
+
setIfAbsent(res, 'Strict-Transport-Security', [
|
|
83
|
+
`max-age=${maxAge}`,
|
|
84
|
+
includeSubDomains ? 'includeSubDomains' : '',
|
|
85
|
+
preload ? 'preload' : '',
|
|
86
|
+
]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join('; '));
|
|
89
|
+
}
|
|
90
|
+
setIfAbsent(res, 'Content-Type', 'text/html; charset=utf-8');
|
|
91
|
+
// Un archivo servido como documento por un visor de otra tecnología puede
|
|
92
|
+
// leer políticas de dominio cruzado que nadie revisó. Cuesta una cabecera.
|
|
93
|
+
setIfAbsent(res, 'X-Permitted-Cross-Domain-Policies', 'none');
|
|
94
|
+
// `X-XSS-Protection` no se emite a propósito: el filtro que activaba está
|
|
95
|
+
// retirado de los navegadores y, donde queda, introduce vulnerabilidades
|
|
96
|
+
// propias. La política de contenido es lo que lo reemplaza.
|
|
97
|
+
// Una página de error guardada por un intermediario es un incidente: el
|
|
98
|
+
// siguiente que pida esa URL recibe el error de otro.
|
|
99
|
+
if (ctx.kind === 'error') {
|
|
100
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// `private` porque el HTML renderizado en el servidor puede llevar datos de
|
|
104
|
+
// la sesión. F-010 lo sobrescribe en las rutas que genere estáticamente.
|
|
105
|
+
setIfAbsent(res, 'Cache-Control', opts.cache.html);
|
|
106
|
+
forbidSharedCacheIfSettingCookies(res, ctx);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pide al navegador las pistas de plataforma que necesita la resolución.
|
|
111
|
+
*
|
|
112
|
+
* `Sec-CH-UA-Form-Factors` es la señal que dice `Desktop`, `Mobile`, `Tablet` o
|
|
113
|
+
* `TV`: exactamente lo que hoy hay que adivinar del User-Agent, pero con cuatro
|
|
114
|
+
* valores posibles en vez de cientos de miles. Es de alta entropía, así que el
|
|
115
|
+
* navegador no la manda salvo que el servidor la pida.
|
|
116
|
+
*
|
|
117
|
+
* `Critical-CH` hace que la **primera** petición se reintente ya con la pista,
|
|
118
|
+
* en lugar de servir una variante adivinada y acertar recién en la segunda.
|
|
119
|
+
* Cuesta un viaje extra la primera vez y sólo se pide en las páginas que varían.
|
|
120
|
+
*
|
|
121
|
+
* Es una mejora para navegadores basados en Chromium. Safari y Firefox no
|
|
122
|
+
* mandan estas pistas, y para ellos el User-Agent sigue siendo la única señal
|
|
123
|
+
* disponible en una navegación directa.
|
|
124
|
+
*/
|
|
125
|
+
function requestClientHints(res) {
|
|
126
|
+
const hints = 'Sec-CH-UA-Platform, Sec-CH-UA-Mobile, Sec-CH-UA-Form-Factors';
|
|
127
|
+
setIfAbsent(res, 'Accept-CH', hints);
|
|
128
|
+
setIfAbsent(res, 'Critical-CH', 'Sec-CH-UA-Form-Factors');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Una respuesta que entrega una cookie no puede guardarse en una caché compartida.
|
|
132
|
+
*
|
|
133
|
+
* Si un intermediario la guarda, el siguiente que pida esa URL recibe **la
|
|
134
|
+
* cookie de otro**: la sesión de una persona pasa a la siguiente. Es de los
|
|
135
|
+
* incidentes más caros que produce una configuración de caché, y ocurre en
|
|
136
|
+
* cuanto alguien pone la caché del HTML en `public` sin acordarse de que un
|
|
137
|
+
* middleware setea una cookie.
|
|
138
|
+
*
|
|
139
|
+
* El default `private` ya lo evita; esto protege al proyecto que lo cambió.
|
|
140
|
+
*/
|
|
141
|
+
function forbidSharedCacheIfSettingCookies(res, ctx) {
|
|
142
|
+
if (!res.getHeader('set-cookie'))
|
|
143
|
+
return;
|
|
144
|
+
const current = String(res.getHeader('Cache-Control') ?? '');
|
|
145
|
+
if (!/\bpublic\b/.test(current))
|
|
146
|
+
return;
|
|
147
|
+
res.setHeader('Cache-Control', current.replace(/\bpublic\b/, 'private'));
|
|
148
|
+
if (!ctx.isProduction) {
|
|
149
|
+
console.warn('[ress] la respuesta entrega una cookie y su Cache-Control decía `public`. ' +
|
|
150
|
+
'Se cambió a `private`: una caché compartida le habría entregado esa cookie ' +
|
|
151
|
+
'a quien pidiera la misma URL después.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function setIfAbsent(res, name, value) {
|
|
155
|
+
if (res.getHeader(name) === undefined)
|
|
156
|
+
res.setHeader(name, value);
|
|
157
|
+
}
|
|
158
|
+
/** Nombre de archivo con un hash de contenido: `algo-A1b2C3d4.js`. */
|
|
159
|
+
const HASHED_NAME_RE = /-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/;
|
|
160
|
+
/**
|
|
161
|
+
* Cuánto puede cachearse un archivo estático.
|
|
162
|
+
*
|
|
163
|
+
* Un nombre con hash de contenido cambia cuando cambia el contenido, así que se
|
|
164
|
+
* puede guardar para siempre. Cualquier otro nombre puede volver a servirse con
|
|
165
|
+
* contenido distinto, y un archivo guardado como inmutable por error es
|
|
166
|
+
* irrecuperable: no hay forma de invalidarlo en el navegador de quien ya lo
|
|
167
|
+
* bajó. Por eso el default es revalidar.
|
|
168
|
+
*/
|
|
169
|
+
function classifyAsset(pathname, buildOutputs) {
|
|
170
|
+
const normalized = pathname.replace(/^\//, '');
|
|
171
|
+
const fileName = normalized.split('/').pop() ?? '';
|
|
172
|
+
// Un documento cacheado de forma inmutable deja la aplicación congelada.
|
|
173
|
+
if (fileName.endsWith('.html'))
|
|
174
|
+
return 'revalidate';
|
|
175
|
+
// El manifest es la fuente autoritativa: son nombres que generó el
|
|
176
|
+
// empaquetador y por construcción llevan el hash del contenido.
|
|
177
|
+
if (buildOutputs.has(normalized))
|
|
178
|
+
return 'immutable';
|
|
179
|
+
// Respaldo para los archivos que emite un plugin sin pasar por el manifest.
|
|
180
|
+
return HASHED_NAME_RE.test(fileName) ? 'immutable' : 'revalidate';
|
|
181
|
+
}
|
|
182
|
+
/** El valor de `Cache-Control` de un archivo estático según su clase. */
|
|
183
|
+
function assetCacheControl(kind, opts) {
|
|
184
|
+
return kind === 'immutable'
|
|
185
|
+
? `public, max-age=${opts.cache.immutableMaxAge}, immutable`
|
|
186
|
+
: `public, max-age=${opts.cache.staticMaxAge}, must-revalidate`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endurecimiento del renderizado en el servidor.
|
|
3
|
+
*
|
|
4
|
+
* Reúne lo que protege una respuesta: qué se serializa hacia el cliente, cómo se
|
|
5
|
+
* escapa lo que llega al documento, qué cabeceras lleva y qué se cuenta cuando
|
|
6
|
+
* algo falla.
|
|
7
|
+
*/
|
|
8
|
+
export { resolveSecurityOptions } from './config';
|
|
9
|
+
export type { ClientPropsPolicy, ResolvedSecurityOptions, SecurityContext, SecurityOptions, } from './config';
|
|
10
|
+
export { escapeAttrValue, escapeHtmlText, renderAttrs, safeAttrName } from './escape';
|
|
11
|
+
export type { EscapeContext } from './escape';
|
|
12
|
+
export { HEAD_TAG_ALLOWLIST, renderHeadTags } from './head-tags';
|
|
13
|
+
export type { HeadTag, HeadTagOptions } from './head-tags';
|
|
14
|
+
export { checkStateSize, renderStateScript, serializeState, SerializationError, } from './serialize';
|
|
15
|
+
export type { SizeCheck, StateBlock } from './serialize';
|
|
16
|
+
export { appendVary, applySecurityHeaders, assetCacheControl, classifyAsset, } from './headers';
|
|
17
|
+
export type { HeaderContext } from './headers';
|
|
18
|
+
export { buildCsp, cspHeaderName, createNonce, injectNonce, reportingEndpointsHeader, } from './csp';
|
|
19
|
+
export type { CspOptions } from './csp';
|
|
20
|
+
export { logServerError, PublicFacingError, sanitizeError } from './errors';
|
|
21
|
+
export type { PublicError } from './errors';
|
|
22
|
+
export { pickClientProps, withheldKeys } from './client-props';
|
|
23
|
+
export { assertDevServerHardening, DENIED_FILES, devServerDefaults } from './dev-hardening';
|
|
24
|
+
export type { DevServerConfig } from './dev-hardening';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Endurecimiento del renderizado en el servidor.
|
|
4
|
+
*
|
|
5
|
+
* Reúne lo que protege una respuesta: qué se serializa hacia el cliente, cómo se
|
|
6
|
+
* escapa lo que llega al documento, qué cabeceras lleva y qué se cuenta cuando
|
|
7
|
+
* algo falla.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.devServerDefaults = exports.DENIED_FILES = exports.assertDevServerHardening = exports.withheldKeys = exports.pickClientProps = exports.sanitizeError = exports.PublicFacingError = exports.logServerError = exports.reportingEndpointsHeader = exports.injectNonce = exports.createNonce = exports.cspHeaderName = exports.buildCsp = exports.classifyAsset = exports.assetCacheControl = exports.applySecurityHeaders = exports.appendVary = exports.SerializationError = exports.serializeState = exports.renderStateScript = exports.checkStateSize = exports.renderHeadTags = exports.HEAD_TAG_ALLOWLIST = exports.safeAttrName = exports.renderAttrs = exports.escapeHtmlText = exports.escapeAttrValue = exports.resolveSecurityOptions = void 0;
|
|
11
|
+
var config_1 = require("./config");
|
|
12
|
+
Object.defineProperty(exports, "resolveSecurityOptions", { enumerable: true, get: function () { return config_1.resolveSecurityOptions; } });
|
|
13
|
+
var escape_1 = require("./escape");
|
|
14
|
+
Object.defineProperty(exports, "escapeAttrValue", { enumerable: true, get: function () { return escape_1.escapeAttrValue; } });
|
|
15
|
+
Object.defineProperty(exports, "escapeHtmlText", { enumerable: true, get: function () { return escape_1.escapeHtmlText; } });
|
|
16
|
+
Object.defineProperty(exports, "renderAttrs", { enumerable: true, get: function () { return escape_1.renderAttrs; } });
|
|
17
|
+
Object.defineProperty(exports, "safeAttrName", { enumerable: true, get: function () { return escape_1.safeAttrName; } });
|
|
18
|
+
var head_tags_1 = require("./head-tags");
|
|
19
|
+
Object.defineProperty(exports, "HEAD_TAG_ALLOWLIST", { enumerable: true, get: function () { return head_tags_1.HEAD_TAG_ALLOWLIST; } });
|
|
20
|
+
Object.defineProperty(exports, "renderHeadTags", { enumerable: true, get: function () { return head_tags_1.renderHeadTags; } });
|
|
21
|
+
var serialize_1 = require("./serialize");
|
|
22
|
+
Object.defineProperty(exports, "checkStateSize", { enumerable: true, get: function () { return serialize_1.checkStateSize; } });
|
|
23
|
+
Object.defineProperty(exports, "renderStateScript", { enumerable: true, get: function () { return serialize_1.renderStateScript; } });
|
|
24
|
+
Object.defineProperty(exports, "serializeState", { enumerable: true, get: function () { return serialize_1.serializeState; } });
|
|
25
|
+
Object.defineProperty(exports, "SerializationError", { enumerable: true, get: function () { return serialize_1.SerializationError; } });
|
|
26
|
+
var headers_1 = require("./headers");
|
|
27
|
+
Object.defineProperty(exports, "appendVary", { enumerable: true, get: function () { return headers_1.appendVary; } });
|
|
28
|
+
Object.defineProperty(exports, "applySecurityHeaders", { enumerable: true, get: function () { return headers_1.applySecurityHeaders; } });
|
|
29
|
+
Object.defineProperty(exports, "assetCacheControl", { enumerable: true, get: function () { return headers_1.assetCacheControl; } });
|
|
30
|
+
Object.defineProperty(exports, "classifyAsset", { enumerable: true, get: function () { return headers_1.classifyAsset; } });
|
|
31
|
+
var csp_1 = require("./csp");
|
|
32
|
+
Object.defineProperty(exports, "buildCsp", { enumerable: true, get: function () { return csp_1.buildCsp; } });
|
|
33
|
+
Object.defineProperty(exports, "cspHeaderName", { enumerable: true, get: function () { return csp_1.cspHeaderName; } });
|
|
34
|
+
Object.defineProperty(exports, "createNonce", { enumerable: true, get: function () { return csp_1.createNonce; } });
|
|
35
|
+
Object.defineProperty(exports, "injectNonce", { enumerable: true, get: function () { return csp_1.injectNonce; } });
|
|
36
|
+
Object.defineProperty(exports, "reportingEndpointsHeader", { enumerable: true, get: function () { return csp_1.reportingEndpointsHeader; } });
|
|
37
|
+
var errors_1 = require("./errors");
|
|
38
|
+
Object.defineProperty(exports, "logServerError", { enumerable: true, get: function () { return errors_1.logServerError; } });
|
|
39
|
+
Object.defineProperty(exports, "PublicFacingError", { enumerable: true, get: function () { return errors_1.PublicFacingError; } });
|
|
40
|
+
Object.defineProperty(exports, "sanitizeError", { enumerable: true, get: function () { return errors_1.sanitizeError; } });
|
|
41
|
+
var client_props_1 = require("./client-props");
|
|
42
|
+
Object.defineProperty(exports, "pickClientProps", { enumerable: true, get: function () { return client_props_1.pickClientProps; } });
|
|
43
|
+
Object.defineProperty(exports, "withheldKeys", { enumerable: true, get: function () { return client_props_1.withheldKeys; } });
|
|
44
|
+
var dev_hardening_1 = require("./dev-hardening");
|
|
45
|
+
Object.defineProperty(exports, "assertDevServerHardening", { enumerable: true, get: function () { return dev_hardening_1.assertDevServerHardening; } });
|
|
46
|
+
Object.defineProperty(exports, "DENIED_FILES", { enumerable: true, get: function () { return dev_hardening_1.DENIED_FILES; } });
|
|
47
|
+
Object.defineProperty(exports, "devServerDefaults", { enumerable: true, get: function () { return dev_hardening_1.devServerDefaults; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialización del estado que viaja al cliente.
|
|
3
|
+
*
|
|
4
|
+
* El JSON se embebe en un `<script>` inline, así que además de ser JSON válido
|
|
5
|
+
* tiene que sobrevivir a dos gramáticas más: la del HTML, que busca `</script`,
|
|
6
|
+
* `<!--` y `<!` dentro del elemento, y la de JavaScript, donde U+2028 y U+2029
|
|
7
|
+
* son terminadores de línea que parten un literal de cadena.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SerializationError extends Error {
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
constructor(reason: string, path: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Serializa un valor para embeberlo en un `<script>` inline.
|
|
16
|
+
*
|
|
17
|
+
* El resultado es JSON válido y `JSON.parse` lo devuelve idéntico al original.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeState(value: unknown, path?: string): string;
|
|
20
|
+
export interface StateBlock {
|
|
21
|
+
/** Nombre de la variable global. Ej: `__RESS_PROPS__`. */
|
|
22
|
+
varName: string;
|
|
23
|
+
value: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* El bloque `<script>` con el estado.
|
|
27
|
+
*
|
|
28
|
+
* Recibe el nonce cuando hay política de contenido: sin él, el bloque no se
|
|
29
|
+
* ejecuta y la hidratación no encuentra sus props.
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderStateScript(blocks: StateBlock[], nonce?: string): string;
|
|
32
|
+
export interface SizeCheck {
|
|
33
|
+
bytes: number;
|
|
34
|
+
overLimit: boolean;
|
|
35
|
+
/** Las claves de primer nivel más pesadas: es lo que hace accionable el aviso. */
|
|
36
|
+
heaviest: Array<{
|
|
37
|
+
key: string;
|
|
38
|
+
bytes: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Cuánto pesa el estado que viaja.
|
|
43
|
+
*
|
|
44
|
+
* Se mide sobre el string **ya serializado y escapado**, que es lo que
|
|
45
|
+
* efectivamente se transfiere, y en bytes y no en caracteres, porque un
|
|
46
|
+
* carácter fuera de ASCII ocupa más de uno.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkStateSize(serialized: string, limitBytes: number, value?: unknown): SizeCheck;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Serialización del estado que viaja al cliente.
|
|
4
|
+
*
|
|
5
|
+
* El JSON se embebe en un `<script>` inline, así que además de ser JSON válido
|
|
6
|
+
* tiene que sobrevivir a dos gramáticas más: la del HTML, que busca `</script`,
|
|
7
|
+
* `<!--` y `<!` dentro del elemento, y la de JavaScript, donde U+2028 y U+2029
|
|
8
|
+
* son terminadores de línea que parten un literal de cadena.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.SerializationError = void 0;
|
|
12
|
+
exports.serializeState = serializeState;
|
|
13
|
+
exports.renderStateScript = renderStateScript;
|
|
14
|
+
exports.checkStateSize = checkStateSize;
|
|
15
|
+
const escape_1 = require("./escape");
|
|
16
|
+
/**
|
|
17
|
+
* Los cinco caracteres a escapar, y ninguno más.
|
|
18
|
+
*
|
|
19
|
+
* Escapar `<` neutraliza `</script>`, `<!--`, `<script` y `<!` de una sola vez,
|
|
20
|
+
* sin tener que reconocer secuencias. `&` va en la lista para que un analizador
|
|
21
|
+
* que decodifique entidades no pueda reconstruir un `<`.
|
|
22
|
+
*
|
|
23
|
+
* Los cinco reemplazos son escapes JSON válidos: `<` **es** `<` para
|
|
24
|
+
* `JSON.parse`, así que el valor no cambia, sólo su representación.
|
|
25
|
+
*/
|
|
26
|
+
const ESCAPE_MAP = {
|
|
27
|
+
'<': '\\u003C',
|
|
28
|
+
'>': '\\u003E',
|
|
29
|
+
'&': '\\u0026',
|
|
30
|
+
'\u2028': '\\u2028',
|
|
31
|
+
'\u2029': '\\u2029',
|
|
32
|
+
};
|
|
33
|
+
const ESCAPE_RE = /[<>&\u2028\u2029]/g;
|
|
34
|
+
class SerializationError extends Error {
|
|
35
|
+
reason;
|
|
36
|
+
path;
|
|
37
|
+
constructor(reason, path) {
|
|
38
|
+
super(`[ress] no se pudo serializar el estado en ${path}: ${reason}`);
|
|
39
|
+
this.reason = reason;
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.name = 'SerializationError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.SerializationError = SerializationError;
|
|
45
|
+
/**
|
|
46
|
+
* Serializa un valor para embeberlo en un `<script>` inline.
|
|
47
|
+
*
|
|
48
|
+
* El resultado es JSON válido y `JSON.parse` lo devuelve idéntico al original.
|
|
49
|
+
*/
|
|
50
|
+
function serializeState(value, path = '$') {
|
|
51
|
+
let json;
|
|
52
|
+
try {
|
|
53
|
+
json = JSON.stringify(value, createGuard(path));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw new SerializationError(describe(err), path);
|
|
57
|
+
}
|
|
58
|
+
if (json === undefined)
|
|
59
|
+
return 'undefined';
|
|
60
|
+
return json.replace(ESCAPE_RE, (c) => ESCAPE_MAP[c]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Detecta lo que no es serializable **antes** de que falle, para poder decir
|
|
64
|
+
* dónde.
|
|
65
|
+
*
|
|
66
|
+
* `JSON.stringify` sobre una estructura cíclica dice "Converting circular
|
|
67
|
+
* structure to JSON" y no nombra la propiedad; sobre una función no dice nada,
|
|
68
|
+
* simplemente la omite. Las dos cosas son difíciles de diagnosticar en una
|
|
69
|
+
* página que dejó de hidratar.
|
|
70
|
+
*
|
|
71
|
+
* `Date` es la excepción deliberada: se convierte a su forma ISO y así llega al
|
|
72
|
+
* cliente. Es el comportamiento de siempre.
|
|
73
|
+
*/
|
|
74
|
+
function createGuard(root) {
|
|
75
|
+
const seen = new WeakSet();
|
|
76
|
+
const paths = new WeakMap();
|
|
77
|
+
return function guard(key, value) {
|
|
78
|
+
const parent = this;
|
|
79
|
+
const path = key === ''
|
|
80
|
+
? root
|
|
81
|
+
: `${(parent && paths.get(parent)) ?? root}${/^\d+$/.test(key) ? `[${key}]` : `.${key}`}`;
|
|
82
|
+
const type = typeof value;
|
|
83
|
+
if (type === 'function')
|
|
84
|
+
throw new SerializationError('es una función', path);
|
|
85
|
+
if (type === 'bigint')
|
|
86
|
+
throw new SerializationError('es un BigInt', path);
|
|
87
|
+
if (type === 'symbol')
|
|
88
|
+
throw new SerializationError('es un Symbol', path);
|
|
89
|
+
if (value instanceof Map)
|
|
90
|
+
throw new SerializationError('es un Map', path);
|
|
91
|
+
if (value instanceof Set)
|
|
92
|
+
throw new SerializationError('es un Set', path);
|
|
93
|
+
if (value && type === 'object' && !(value instanceof Date)) {
|
|
94
|
+
if (seen.has(value)) {
|
|
95
|
+
throw new SerializationError('la estructura tiene un ciclo', path);
|
|
96
|
+
}
|
|
97
|
+
seen.add(value);
|
|
98
|
+
paths.set(value, path);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function describe(err) {
|
|
104
|
+
if (err instanceof SerializationError)
|
|
105
|
+
throw err;
|
|
106
|
+
return err instanceof Error ? err.message : String(err);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* El bloque `<script>` con el estado.
|
|
110
|
+
*
|
|
111
|
+
* Recibe el nonce cuando hay política de contenido: sin él, el bloque no se
|
|
112
|
+
* ejecuta y la hidratación no encuentra sus props.
|
|
113
|
+
*/
|
|
114
|
+
function renderStateScript(blocks, nonce) {
|
|
115
|
+
const attrs = nonce ? ` nonce="${(0, escape_1.escapeAttrValue)(nonce)}"` : '';
|
|
116
|
+
const body = blocks
|
|
117
|
+
.map((b) => `window.${b.varName}=${serializeState(b.value, '$.' + b.varName)};`)
|
|
118
|
+
.join('');
|
|
119
|
+
return `<script${attrs}>${body}</script>`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Cuánto pesa el estado que viaja.
|
|
123
|
+
*
|
|
124
|
+
* Se mide sobre el string **ya serializado y escapado**, que es lo que
|
|
125
|
+
* efectivamente se transfiere, y en bytes y no en caracteres, porque un
|
|
126
|
+
* carácter fuera de ASCII ocupa más de uno.
|
|
127
|
+
*/
|
|
128
|
+
function checkStateSize(serialized, limitBytes, value) {
|
|
129
|
+
const bytes = Buffer.byteLength(serialized, 'utf8');
|
|
130
|
+
const overLimit = bytes > limitBytes;
|
|
131
|
+
const heaviest = overLimit && value && typeof value === 'object'
|
|
132
|
+
? Object.entries(value)
|
|
133
|
+
.map(([key, v]) => ({ key, bytes: safeSize(v) }))
|
|
134
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
135
|
+
.slice(0, 3)
|
|
136
|
+
: [];
|
|
137
|
+
return { bytes, overLimit, heaviest };
|
|
138
|
+
}
|
|
139
|
+
function safeSize(value) {
|
|
140
|
+
try {
|
|
141
|
+
return Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AssetIndex, VariantCatalog } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Artefactos en desarrollo.
|
|
4
|
+
*
|
|
5
|
+
* Vite sirve el módulo virtual directamente, así que el guion es su propia URL.
|
|
6
|
+
* Los estilos no se listan porque el módulo los importa y Vite los inyecta.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createDevAssetIndex(catalog: VariantCatalog, base?: string): AssetIndex;
|
|
9
|
+
/** Forma del manifest que emite el entorno de cliente. */
|
|
10
|
+
interface ManifestChunk {
|
|
11
|
+
file: string;
|
|
12
|
+
css?: string[];
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Artefactos en producción, tomados del manifest del cliente.
|
|
17
|
+
*
|
|
18
|
+
* El manifest se lee una vez al arrancar y se pasa a un índice por página y
|
|
19
|
+
* variante, de modo que servir una petición sea una consulta en memoria y no una
|
|
20
|
+
* búsqueda.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createProdAssetIndex(catalog: VariantCatalog, manifest: Record<string, ManifestChunk>, base?: string): AssetIndex;
|
|
23
|
+
export {};
|