@ressjs/vite-router 0.5.2 → 0.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +69 -0
- package/README.md +9 -515
- package/dist/client/entry.d.mts +2 -0
- package/dist/client/entry.mjs +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/config/base-path.d.ts +44 -0
- package/dist/config/base-path.js +100 -0
- package/dist/config/codegen.d.ts +29 -0
- package/dist/config/codegen.js +131 -0
- package/dist/config/define.d.ts +8 -0
- package/dist/config/define.js +12 -0
- package/dist/config/env.d.ts +19 -0
- package/dist/config/env.js +40 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/config/index.js +46 -0
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +82 -0
- package/dist/config/middleware.d.ts +18 -0
- package/dist/config/middleware.js +79 -0
- package/dist/config/rules.d.ts +59 -0
- package/dist/config/rules.js +162 -0
- package/dist/config/types.d.ts +189 -0
- package/dist/config/types.js +2 -0
- package/dist/config/validate.d.ts +19 -0
- package/dist/config/validate.js +143 -0
- package/dist/config/watch.d.ts +53 -0
- package/dist/config/watch.js +163 -0
- package/dist/fs/module-extensions.d.ts +105 -0
- package/dist/fs/module-extensions.js +213 -0
- package/dist/head/resolve.d.ts +5 -0
- package/dist/head/resolve.js +166 -0
- package/dist/head/types.d.ts +44 -0
- package/dist/head/types.js +2 -0
- package/dist/helpers/html-generator.d.ts +26 -6
- package/dist/helpers/html-generator.js +96 -138
- package/dist/helpers/middlewares.d.ts +32 -21
- package/dist/helpers/middlewares.js +181 -159
- package/dist/helpers/page-config-merge.d.ts +11 -0
- package/dist/helpers/page-config-merge.js +84 -0
- package/dist/helpers/request-handler.d.ts +15 -2
- package/dist/helpers/request-handler.js +164 -27
- package/dist/index.d.ts +35 -3
- package/dist/index.js +77 -8
- package/dist/isr/capture.d.ts +79 -0
- package/dist/isr/capture.js +222 -0
- package/dist/isr/handler.d.ts +49 -0
- package/dist/isr/handler.js +207 -0
- package/dist/isr/key.d.ts +26 -0
- package/dist/isr/key.js +59 -0
- package/dist/isr/page-config.d.ts +17 -0
- package/dist/isr/page-config.js +71 -0
- package/dist/isr/preview.d.ts +5 -0
- package/dist/isr/preview.js +38 -0
- package/dist/isr/public-request.d.ts +50 -0
- package/dist/isr/public-request.js +108 -0
- package/dist/isr/response.d.ts +41 -0
- package/dist/isr/response.js +128 -0
- package/dist/isr/store.d.ts +42 -0
- package/dist/isr/store.js +108 -0
- package/dist/isr/types.d.ts +62 -0
- package/dist/isr/types.js +2 -0
- package/dist/pages.d.ts +10 -9
- package/dist/pages.js +42 -261
- package/dist/platform.d.ts +11 -14
- package/dist/platform.js +18 -102
- package/dist/plugin/environments.d.ts +68 -0
- package/dist/plugin/environments.js +74 -0
- package/dist/plugin/index.d.ts +59 -0
- package/dist/plugin/index.js +195 -0
- package/dist/plugin/route-manifest.d.ts +19 -0
- package/dist/plugin/route-manifest.js +55 -0
- package/dist/plugin/virtual-entries.d.ts +26 -0
- package/dist/plugin/virtual-entries.js +80 -0
- package/dist/render.d.ts +39 -2
- package/dist/render.js +75 -77
- package/dist/router.d.ts +32 -13
- package/dist/router.js +180 -146
- package/dist/routes/dispatcher.d.ts +58 -0
- package/dist/routes/dispatcher.js +70 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +62 -0
- package/dist/routes/match.d.ts +20 -0
- package/dist/routes/match.js +75 -0
- package/dist/routes/module.d.ts +14 -0
- package/dist/routes/module.js +30 -0
- package/dist/routes/parse.d.ts +31 -0
- package/dist/routes/parse.js +114 -0
- package/dist/routes/rank.d.ts +12 -0
- package/dist/routes/rank.js +49 -0
- package/dist/routes/router.d.ts +54 -0
- package/dist/routes/router.js +153 -0
- package/dist/routes/scan.d.ts +26 -0
- package/dist/routes/scan.js +98 -0
- package/dist/routes/types.d.ts +114 -0
- package/dist/routes/types.js +17 -0
- package/dist/runtime/dev-server.d.ts +64 -0
- package/dist/runtime/dev-server.js +94 -0
- package/dist/runtime/dev-styles.d.ts +13 -0
- package/dist/runtime/dev-styles.js +50 -0
- package/dist/runtime/module-loader.d.ts +55 -0
- package/dist/runtime/module-loader.js +122 -0
- package/dist/runtime/prod-server.d.ts +55 -0
- package/dist/runtime/prod-server.js +188 -0
- package/dist/runtime/template.d.ts +10 -0
- package/dist/runtime/template.js +33 -0
- package/dist/security/client-props.d.ts +25 -0
- package/dist/security/client-props.js +51 -0
- package/dist/security/config.d.ts +121 -0
- package/dist/security/config.js +77 -0
- package/dist/security/csp.d.ts +53 -0
- package/dist/security/csp.js +118 -0
- package/dist/security/dev-hardening.d.ts +46 -0
- package/dist/security/dev-hardening.js +65 -0
- package/dist/security/errors.d.ts +37 -0
- package/dist/security/errors.js +84 -0
- package/dist/security/escape.d.ts +40 -0
- package/dist/security/escape.js +90 -0
- package/dist/security/head-tags.d.ts +32 -0
- package/dist/security/head-tags.js +156 -0
- package/dist/security/headers.d.ts +76 -0
- package/dist/security/headers.js +278 -0
- package/dist/security/index.d.ts +24 -0
- package/dist/security/index.js +49 -0
- package/dist/security/serialize.d.ts +48 -0
- package/dist/security/serialize.js +146 -0
- package/dist/variants/assets.d.ts +33 -0
- package/dist/variants/assets.js +144 -0
- package/dist/variants/catalog.d.ts +33 -0
- package/dist/variants/catalog.js +79 -0
- package/dist/variants/platform-tokens.d.ts +9 -0
- package/dist/variants/platform-tokens.js +15 -0
- package/dist/variants/resolve.d.ts +40 -0
- package/dist/variants/resolve.js +90 -0
- package/dist/variants/suffix.d.ts +8 -0
- package/dist/variants/suffix.js +12 -0
- package/dist/variants/types.d.ts +72 -0
- package/dist/variants/types.js +2 -0
- package/package.json +23 -11
- package/dist/helpers/vite-config.d.ts +0 -10
- package/dist/helpers/vite-config.js +0 -90
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RouteSegment } from '../routes/types';
|
|
2
|
+
import type { HeaderRule, RedirectRule, ResolvedConfig, RewriteRule } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Redirecciones, reescrituras y cabeceras declaradas por el proyecto.
|
|
5
|
+
*
|
|
6
|
+
* Las tres comparten patrón, y el patrón es **el mismo analizador de rutas** que
|
|
7
|
+
* usa el manifest: `matchSegments()` recorre segmento a segmento y exige consumir
|
|
8
|
+
* la URL entera. Una segunda implementación de coincidencia acá significaría que
|
|
9
|
+
* `/docs/:seccion` puede querer decir dos cosas distintas según quién la lea.
|
|
10
|
+
*
|
|
11
|
+
* Notación de un patrón:
|
|
12
|
+
*
|
|
13
|
+
* | Escrito | Coincide con |
|
|
14
|
+
* |---|---|
|
|
15
|
+
* | `/precios` | exactamente `/precios` |
|
|
16
|
+
* | `/blog/:slug` | un segmento, disponible como `:slug` en el destino |
|
|
17
|
+
* | `/docs/:...resto` | uno o más segmentos |
|
|
18
|
+
* | `/docs/:...resto?` | cero o más segmentos |
|
|
19
|
+
*/
|
|
20
|
+
export interface CompiledRule<T> {
|
|
21
|
+
rule: T;
|
|
22
|
+
source: string;
|
|
23
|
+
segments: RouteSegment[];
|
|
24
|
+
paramNames: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface CompiledRules {
|
|
27
|
+
redirects: CompiledRule<RedirectRule>[];
|
|
28
|
+
rewrites: CompiledRule<RewriteRule>[];
|
|
29
|
+
headers: CompiledRule<HeaderRule>[];
|
|
30
|
+
}
|
|
31
|
+
export declare function parseRulePattern(source: string, label: string): {
|
|
32
|
+
segments: RouteSegment[];
|
|
33
|
+
paramNames: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Compila las tres listas y comprueba lo que sólo se puede comprobar con los
|
|
37
|
+
* patrones ya analizados: parámetros que el destino usa sin que el origen los
|
|
38
|
+
* declare, cabeceras reservadas, y ciclos de redirección.
|
|
39
|
+
*/
|
|
40
|
+
export declare function compileRules(config: ResolvedConfig): CompiledRules;
|
|
41
|
+
export interface RuleMatch<T> {
|
|
42
|
+
compiled: CompiledRule<T>;
|
|
43
|
+
params: Record<string, string | string[]>;
|
|
44
|
+
}
|
|
45
|
+
/** La primera regla que atiende esta ruta, en el orden en que se declararon. */
|
|
46
|
+
export declare function matchRule<T>(rules: readonly CompiledRule<T>[], pathname: string): RuleMatch<T> | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Traslada los parámetros que capturó el origen al destino.
|
|
49
|
+
*
|
|
50
|
+
* `matchSegments()` entrega los valores ya decodificados (R-8): un segmento
|
|
51
|
+
* como `%2F%2Fevil.example` llega acá como `//evil.example`. Si se pegara tal
|
|
52
|
+
* cual, `/old/:p` con ese pedido armaría el destino `//evil.example`, que
|
|
53
|
+
* `absolute()` (en `middleware.ts`) lee como una URL absoluta y termina en un
|
|
54
|
+
* `Location` que saca al visitante del origen — un open redirect. Por eso cada
|
|
55
|
+
* segmento se vuelve a codificar con `encodeURIComponent`: una barra decodificada
|
|
56
|
+
* nunca sobrevive para armar un segmento nuevo, y un catch-all de varios
|
|
57
|
+
* segmentos se preserva uniendo cada uno ya codificado con `/`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyParams(destination: string, params: Record<string, string | string[]>): string;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRulePattern = parseRulePattern;
|
|
4
|
+
exports.compileRules = compileRules;
|
|
5
|
+
exports.matchRule = matchRule;
|
|
6
|
+
exports.applyParams = applyParams;
|
|
7
|
+
const match_1 = require("../routes/match");
|
|
8
|
+
/** Cabeceras que administra el framework: declararlas desde el proyecto las rompe. */
|
|
9
|
+
const RESERVED_HEADERS = new Set(['vary', 'content-type', 'content-length', 'transfer-encoding']);
|
|
10
|
+
function parseRulePattern(source, label) {
|
|
11
|
+
if (!source.startsWith('/')) {
|
|
12
|
+
throw new Error(`[ress] ${label}: el patrón "${source}" tiene que empezar con "/". ` +
|
|
13
|
+
'Es una ruta de esta aplicación, no una URL relativa.');
|
|
14
|
+
}
|
|
15
|
+
const segments = [];
|
|
16
|
+
const paramNames = [];
|
|
17
|
+
// El patrón se parte a mano y no con `splitPath()`: esa función descarta la
|
|
18
|
+
// consulta a partir del `?`, que acá es la marca de catch-all opcional.
|
|
19
|
+
for (const raw of source.split('/').filter(Boolean)) {
|
|
20
|
+
const previous = segments[segments.length - 1];
|
|
21
|
+
if (previous && (previous.kind === 'catchAll' || previous.kind === 'optionalCatchAll')) {
|
|
22
|
+
throw new Error(`[ress] ${label}: "${source}" pone un segmento después de un catch-all, ` +
|
|
23
|
+
'que ya consume el resto de la URL.');
|
|
24
|
+
}
|
|
25
|
+
if (!raw.startsWith(':')) {
|
|
26
|
+
segments.push({ kind: 'static', value: raw });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const optional = raw.endsWith('?');
|
|
30
|
+
const body = optional ? raw.slice(1, -1) : raw.slice(1);
|
|
31
|
+
const catchAll = body.startsWith('...');
|
|
32
|
+
const name = catchAll ? body.slice(3) : body;
|
|
33
|
+
if (!name) {
|
|
34
|
+
throw new Error(`[ress] ${label}: "${source}" declara un parámetro sin nombre.`);
|
|
35
|
+
}
|
|
36
|
+
if (optional && !catchAll) {
|
|
37
|
+
throw new Error(`[ress] ${label}: "${source}" marca ":${name}" como opcional. ` +
|
|
38
|
+
'Sólo un catch-all puede serlo: ":...' + name + '?".');
|
|
39
|
+
}
|
|
40
|
+
if (paramNames.includes(name)) {
|
|
41
|
+
throw new Error(`[ress] ${label}: "${source}" usa el parámetro ":${name}" dos veces.`);
|
|
42
|
+
}
|
|
43
|
+
paramNames.push(name);
|
|
44
|
+
segments.push({
|
|
45
|
+
kind: catchAll ? (optional ? 'optionalCatchAll' : 'catchAll') : 'dynamic',
|
|
46
|
+
value: name,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return { segments, paramNames };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compila las tres listas y comprueba lo que sólo se puede comprobar con los
|
|
53
|
+
* patrones ya analizados: parámetros que el destino usa sin que el origen los
|
|
54
|
+
* declare, cabeceras reservadas, y ciclos de redirección.
|
|
55
|
+
*/
|
|
56
|
+
function compileRules(config) {
|
|
57
|
+
const compiled = {
|
|
58
|
+
redirects: compileList(config.redirects, 'redirects'),
|
|
59
|
+
rewrites: compileList(config.rewrites, 'rewrites'),
|
|
60
|
+
headers: compileList(config.headers, 'headers'),
|
|
61
|
+
};
|
|
62
|
+
const problems = [];
|
|
63
|
+
for (const [index, entry] of compiled.redirects.entries()) {
|
|
64
|
+
checkDestination(entry, entry.rule.destination, `redirects[${index}]`, problems);
|
|
65
|
+
}
|
|
66
|
+
for (const [index, entry] of compiled.rewrites.entries()) {
|
|
67
|
+
checkDestination(entry, entry.rule.destination, `rewrites[${index}]`, problems);
|
|
68
|
+
}
|
|
69
|
+
for (const [index, entry] of compiled.headers.entries()) {
|
|
70
|
+
for (const header of entry.rule.headers) {
|
|
71
|
+
if (RESERVED_HEADERS.has(header.key.toLowerCase())) {
|
|
72
|
+
problems.push(`headers[${index}]: "${header.key}" la administra el framework y no se puede declarar acá.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const cycle = findRedirectCycle(compiled.redirects);
|
|
77
|
+
if (cycle) {
|
|
78
|
+
problems.push(`redirects: las reglas ${cycle.join(' → ')} forman un ciclo. ` +
|
|
79
|
+
'Un ciclo deja al navegador saltando hasta que se rinde.');
|
|
80
|
+
}
|
|
81
|
+
if (problems.length) {
|
|
82
|
+
throw new Error('[ress] La configuración tiene reglas inválidas:\n' +
|
|
83
|
+
problems.map((p) => ` · ${p}`).join('\n'));
|
|
84
|
+
}
|
|
85
|
+
return compiled;
|
|
86
|
+
}
|
|
87
|
+
function compileList(rules, label) {
|
|
88
|
+
return rules.map((rule, index) => {
|
|
89
|
+
const { segments, paramNames } = parseRulePattern(rule.source, `${label}[${index}]`);
|
|
90
|
+
return { rule, source: rule.source, segments, paramNames };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Todo `:param` del destino tiene que estar declarado en el origen. */
|
|
94
|
+
function checkDestination(entry, destination, label, problems) {
|
|
95
|
+
for (const used of destination.matchAll(/:(\.{3})?([A-Za-z0-9_]+)/g)) {
|
|
96
|
+
const name = used[2];
|
|
97
|
+
if (!entry.paramNames.includes(name)) {
|
|
98
|
+
problems.push(`${label}.destination: usa ":${name}", que "${entry.source}" no declara. ` +
|
|
99
|
+
`Declarados: ${entry.paramNames.map((p) => ':' + p).join(', ') || 'ninguno'}.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Un ciclo de redirecciones.
|
|
105
|
+
*
|
|
106
|
+
* Se sigue la cadena desde cada regla: se toma su destino, se busca qué regla lo
|
|
107
|
+
* atendería, y así. Volver a pisar una regla ya visitada en la misma cadena es un
|
|
108
|
+
* ciclo. Sólo se puede seguir un destino sin parámetros sin inventar valores, así
|
|
109
|
+
* que la comprobación es de los ciclos que se pueden demostrar.
|
|
110
|
+
*/
|
|
111
|
+
function findRedirectCycle(redirects) {
|
|
112
|
+
for (const start of redirects) {
|
|
113
|
+
const seen = [start.source];
|
|
114
|
+
let current = start;
|
|
115
|
+
for (let step = 0; step < redirects.length; step++) {
|
|
116
|
+
const destination = current.rule.destination;
|
|
117
|
+
if (destination.includes(':'))
|
|
118
|
+
break;
|
|
119
|
+
const next = matchRule(redirects, destination);
|
|
120
|
+
if (!next)
|
|
121
|
+
break;
|
|
122
|
+
if (seen.includes(next.compiled.source)) {
|
|
123
|
+
return [...seen, next.compiled.source];
|
|
124
|
+
}
|
|
125
|
+
seen.push(next.compiled.source);
|
|
126
|
+
current = next.compiled;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/** La primera regla que atiende esta ruta, en el orden en que se declararon. */
|
|
132
|
+
function matchRule(rules, pathname) {
|
|
133
|
+
const parts = (0, match_1.splitPath)(pathname);
|
|
134
|
+
for (const compiled of rules) {
|
|
135
|
+
const params = (0, match_1.matchSegments)(compiled.segments, parts);
|
|
136
|
+
if (params)
|
|
137
|
+
return { compiled, params };
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Traslada los parámetros que capturó el origen al destino.
|
|
143
|
+
*
|
|
144
|
+
* `matchSegments()` entrega los valores ya decodificados (R-8): un segmento
|
|
145
|
+
* como `%2F%2Fevil.example` llega acá como `//evil.example`. Si se pegara tal
|
|
146
|
+
* cual, `/old/:p` con ese pedido armaría el destino `//evil.example`, que
|
|
147
|
+
* `absolute()` (en `middleware.ts`) lee como una URL absoluta y termina en un
|
|
148
|
+
* `Location` que saca al visitante del origen — un open redirect. Por eso cada
|
|
149
|
+
* segmento se vuelve a codificar con `encodeURIComponent`: una barra decodificada
|
|
150
|
+
* nunca sobrevive para armar un segmento nuevo, y un catch-all de varios
|
|
151
|
+
* segmentos se preserva uniendo cada uno ya codificado con `/`.
|
|
152
|
+
*/
|
|
153
|
+
function applyParams(destination, params) {
|
|
154
|
+
return destination.replace(/:(\.{3})?([A-Za-z0-9_]+)/g, (whole, _spread, name) => {
|
|
155
|
+
const value = params[name];
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
return whole;
|
|
158
|
+
return Array.isArray(value)
|
|
159
|
+
? value.map((segment) => encodeURIComponent(segment)).join('/')
|
|
160
|
+
: encodeURIComponent(value);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { ExtensionPolicy } from '../fs/module-extensions';
|
|
2
|
+
import type { SecurityOptions } from '../security';
|
|
3
|
+
/**
|
|
4
|
+
* La configuración del proyecto: un objeto por archivo de `config/`.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* // config/production.js
|
|
8
|
+
* export default {
|
|
9
|
+
* basePath: '/app',
|
|
10
|
+
* redirects: () => [{ source: '/viejo', destination: '/nuevo', permanent: true }],
|
|
11
|
+
* security: { csp: { enabled: true } },
|
|
12
|
+
*
|
|
13
|
+
* // Y las claves propias del proyecto, que conviven con las del framework.
|
|
14
|
+
* API_URL: 'https://api.example.com',
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Las claves que el framework reconoce están declaradas abajo; cualquier otra es
|
|
19
|
+
* del proyecto y se lee con `import { config } from '@ressjs/config'`. No hay un
|
|
20
|
+
* segundo archivo para eso: un solo lugar donde buscar de dónde sale un valor.
|
|
21
|
+
*
|
|
22
|
+
* Cada spec que necesita configuración aporta su propia interfaz y la enchufa
|
|
23
|
+
* como una clave. Esta spec aporta el continente, la carga, la fusión y la
|
|
24
|
+
* validación; no el contenido de cada sección.
|
|
25
|
+
*/
|
|
26
|
+
export type TrailingSlashMode = 'always' | 'never' | 'ignore';
|
|
27
|
+
export interface RessConfig {
|
|
28
|
+
/** Prefijo bajo el que se sirve la aplicación. Sin barra final. */
|
|
29
|
+
basePath?: string;
|
|
30
|
+
/** Qué hacer con la barra final de una URL. */
|
|
31
|
+
trailingSlash?: TrailingSlashMode;
|
|
32
|
+
redirects?: () => RedirectRule[] | Promise<RedirectRule[]>;
|
|
33
|
+
rewrites?: () => RewriteRule[] | Promise<RewriteRule[]>;
|
|
34
|
+
headers?: () => HeaderRule[] | Promise<HeaderRule[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Variables de entorno que llegan al navegador.
|
|
37
|
+
*
|
|
38
|
+
* Lista explícita y no un prefijo: un prefijo publica lo que alguien agregue
|
|
39
|
+
* después sin que nadie lo decida.
|
|
40
|
+
*/
|
|
41
|
+
publicEnv?: string[];
|
|
42
|
+
/** Extensiones reconocidas como código de usuario. F-017. */
|
|
43
|
+
extensions?: Partial<ExtensionPolicy>;
|
|
44
|
+
/** Tokens de plataforma propios del proyecto. Alimenta sólo los tipos generados; F-024. */
|
|
45
|
+
platform?: PlatformConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Cabeceras, política de contenido y límites.
|
|
48
|
+
*
|
|
49
|
+
* Se carga, se fusiona y se valida como el resto de la configuración.
|
|
50
|
+
* `createViteRouter`/`ressCorePlugin` la leen después de `loadConfig` para
|
|
51
|
+
* completar los defaults de seguridad: sólo se pisa si quien arma el router
|
|
52
|
+
* o el plugin aportó `security`/`securityOptions` explícito. F-019.
|
|
53
|
+
*/
|
|
54
|
+
security?: SecurityOptions;
|
|
55
|
+
/** Prefijo de CDN para archivos estáticos. Consumido por F-006. */
|
|
56
|
+
assets?: AssetsConfig;
|
|
57
|
+
/** Caché incremental de páginas `renderingMode: 'isr'`. F-011. */
|
|
58
|
+
isr?: IsrConfig;
|
|
59
|
+
/**
|
|
60
|
+
* Vía de escape nivel 1 (F-033): extender la aplicación de servidor sin
|
|
61
|
+
* salir de los comandos de la CLI.
|
|
62
|
+
*/
|
|
63
|
+
server?: RessServerConfig;
|
|
64
|
+
/**
|
|
65
|
+
* Capas por modo, sólo en la forma de archivo único (`config.js` en vez de la
|
|
66
|
+
* carpeta). La forma de carpeta las expresa con un archivo por modo, que es la
|
|
67
|
+
* que conviene cuando la configuración crece.
|
|
68
|
+
*/
|
|
69
|
+
development?: ModeOverrides;
|
|
70
|
+
production?: ModeOverrides;
|
|
71
|
+
local?: ModeOverrides;
|
|
72
|
+
/**
|
|
73
|
+
* Los valores propios del proyecto: una URL de API, una clave, un feature
|
|
74
|
+
* flag. Conviven con las claves del framework en el mismo objeto y se leen con
|
|
75
|
+
* `import { config } from '@ressjs/config'`.
|
|
76
|
+
*/
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
/** Una capa de modo no puede contener otras capas. */
|
|
80
|
+
export type ModeOverrides = Omit<RessConfig, 'development' | 'production' | 'local'>;
|
|
81
|
+
export interface AssetsConfig {
|
|
82
|
+
/** Prefijo absoluto desde el que se sirven los artefactos. */
|
|
83
|
+
cdnPrefix?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface IsrConfig {
|
|
86
|
+
/** Máximo de entradas en memoria. Por defecto 500. */
|
|
87
|
+
maxEntries?: number;
|
|
88
|
+
/** Máximo de bytes de HTML en memoria. Por defecto 50 MiB. */
|
|
89
|
+
maxBytes?: number;
|
|
90
|
+
/** Tiempo máximo de una generación, en milisegundos. Por defecto 30 000. */
|
|
91
|
+
generationTimeoutMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Usar la caché real en `ress dev`. Por defecto `false`: en desarrollo la
|
|
94
|
+
* página se renderiza en cada petición, bajo el mismo contrato público.
|
|
95
|
+
*/
|
|
96
|
+
dev?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Cuándo se le dice a una CDN que puede guardar una página ISR
|
|
99
|
+
* (`Cache-Control: public, s-maxage=…`).
|
|
100
|
+
*
|
|
101
|
+
* - `auto` (por defecto): sólo en rutas sin middlewares. Una CDN no corre
|
|
102
|
+
* los middlewares de ress.js: si uno protege la ruta, la CDN serviría a
|
|
103
|
+
* cualquiera la respuesta que guardó de alguien autorizado.
|
|
104
|
+
* - `always`: en toda página ISR. Para quien sabe que sus middlewares no
|
|
105
|
+
* deciden quién ve la página (logs, métricas, cabeceras).
|
|
106
|
+
* - `never`: nunca. La caché de ress.js sigue funcionando igual.
|
|
107
|
+
*/
|
|
108
|
+
sharedCache?: 'auto' | 'always' | 'never';
|
|
109
|
+
/**
|
|
110
|
+
* Secreto de vista previa: con él, la cabecera `x-ress-preview` o la cookie
|
|
111
|
+
* `__ress_preview` renderizan la página con la petición completa, sin tocar
|
|
112
|
+
* la caché. Sin valor, la vista previa no existe.
|
|
113
|
+
*/
|
|
114
|
+
previewSecret?: string;
|
|
115
|
+
}
|
|
116
|
+
export type ResolvedIsrConfig = Required<Omit<IsrConfig, 'previewSecret'>> & Pick<IsrConfig, 'previewSecret'>;
|
|
117
|
+
/**
|
|
118
|
+
* Vía de escape nivel 1: extender la aplicación de servidor desde la
|
|
119
|
+
* configuración, sin dejar de usar `ress dev` / `ress build` / `ress start`.
|
|
120
|
+
*
|
|
121
|
+
* Las funciones reciben el `Express` que construye `createRessApp` (F-033).
|
|
122
|
+
* `beforeRouter` corre antes de montar el enrutador del framework y gana ante
|
|
123
|
+
* sus rutas; `afterRouter` corre después y no puede pisarlas.
|
|
124
|
+
*/
|
|
125
|
+
export interface RessServerConfig {
|
|
126
|
+
/** Se ejecuta antes de montar el enrutador del framework. Gana ante rutas del framework. */
|
|
127
|
+
beforeRouter?: (app: import('express').Express) => void | Promise<void>;
|
|
128
|
+
/** Se ejecuta después. No puede pisar rutas del framework. */
|
|
129
|
+
afterRouter?: (app: import('express').Express) => void | Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Tokens de plataforma propios del proyecto, para el autocompletado.
|
|
133
|
+
*
|
|
134
|
+
* `detect`, `axisPrecedence` y `edge` no están acá: el motor de detección los
|
|
135
|
+
* lee de `defaultPlatformRegistry`, no de esta configuración. Declararlos aquí
|
|
136
|
+
* sin que nada los consuma es la clase de clave que promete algo que no hace.
|
|
137
|
+
*/
|
|
138
|
+
export interface PlatformConfig {
|
|
139
|
+
values?: Array<{
|
|
140
|
+
token: string;
|
|
141
|
+
axis: string;
|
|
142
|
+
label?: string;
|
|
143
|
+
}>;
|
|
144
|
+
/** Plataformas para las que se prepara el proyecto generado. F-033. */
|
|
145
|
+
targets?: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Intención declarativa de usar clientes completos forkeables. Las plantillas
|
|
148
|
+
* y sus runtimes pertenecen a F-028 (React Native) y F-029 (Electron).
|
|
149
|
+
*/
|
|
150
|
+
forkedClients?: string[];
|
|
151
|
+
}
|
|
152
|
+
export interface RedirectRule {
|
|
153
|
+
source: string;
|
|
154
|
+
destination: string;
|
|
155
|
+
/** Permanente emite 308; temporal, 307. */
|
|
156
|
+
permanent?: boolean;
|
|
157
|
+
}
|
|
158
|
+
export interface RewriteRule {
|
|
159
|
+
source: string;
|
|
160
|
+
destination: string;
|
|
161
|
+
}
|
|
162
|
+
export interface HeaderRule {
|
|
163
|
+
source: string;
|
|
164
|
+
headers: Array<{
|
|
165
|
+
key: string;
|
|
166
|
+
value: string;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* La configuración ya resuelta.
|
|
171
|
+
*
|
|
172
|
+
* Todas las claves que el framework consulta existen, así que nadie tiene que
|
|
173
|
+
* escribir `config.security ?? {}` en cada uso. Las capas de modo desaparecen:
|
|
174
|
+
* ya se fusionaron.
|
|
175
|
+
*/
|
|
176
|
+
export interface ResolvedConfig {
|
|
177
|
+
basePath: string;
|
|
178
|
+
trailingSlash: TrailingSlashMode;
|
|
179
|
+
redirects: RedirectRule[];
|
|
180
|
+
rewrites: RewriteRule[];
|
|
181
|
+
headers: HeaderRule[];
|
|
182
|
+
publicEnv: string[];
|
|
183
|
+
extensions: Partial<ExtensionPolicy>;
|
|
184
|
+
platform: PlatformConfig;
|
|
185
|
+
security: SecurityOptions;
|
|
186
|
+
assets: Required<AssetsConfig> | AssetsConfig;
|
|
187
|
+
isr: ResolvedIsrConfig;
|
|
188
|
+
server: RessServerConfig;
|
|
189
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HeaderRule, RedirectRule, RessConfig, RewriteRule } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Comprueba la configuración al arrancar.
|
|
4
|
+
*
|
|
5
|
+
* Un valor mal escrito acá se manifiesta como una petición que responde raro
|
|
6
|
+
* mucho después y en otro lugar. Fallar al arrancar, nombrando la clave, es la
|
|
7
|
+
* diferencia entre corregir un error en un minuto y buscarlo durante una tarde.
|
|
8
|
+
*
|
|
9
|
+
* Se acumulan todos los problemas antes de fallar: arreglar de a uno, reiniciar,
|
|
10
|
+
* y encontrar el siguiente es una forma lenta de perder la tarde.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateConfig(config: RessConfig): void;
|
|
13
|
+
/**
|
|
14
|
+
* Comprueba las reglas que devolvieron `redirects`, `rewrites` y `headers`.
|
|
15
|
+
*
|
|
16
|
+
* Se hace después de invocarlas, no en `validateConfig`, porque hasta ese momento
|
|
17
|
+
* son funciones sin ejecutar.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateRules(redirects: RedirectRule[], rewrites: RewriteRule[], headers: HeaderRule[]): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateConfig = validateConfig;
|
|
4
|
+
exports.validateRules = validateRules;
|
|
5
|
+
/**
|
|
6
|
+
* Comprueba la configuración al arrancar.
|
|
7
|
+
*
|
|
8
|
+
* Un valor mal escrito acá se manifiesta como una petición que responde raro
|
|
9
|
+
* mucho después y en otro lugar. Fallar al arrancar, nombrando la clave, es la
|
|
10
|
+
* diferencia entre corregir un error en un minuto y buscarlo durante una tarde.
|
|
11
|
+
*
|
|
12
|
+
* Se acumulan todos los problemas antes de fallar: arreglar de a uno, reiniciar,
|
|
13
|
+
* y encontrar el siguiente es una forma lenta de perder la tarde.
|
|
14
|
+
*/
|
|
15
|
+
function validateConfig(config) {
|
|
16
|
+
const problems = [];
|
|
17
|
+
checkBasePath(config, problems);
|
|
18
|
+
checkTrailingSlash(config, problems);
|
|
19
|
+
checkPublicEnv(config, problems);
|
|
20
|
+
checkCallable(config, problems);
|
|
21
|
+
checkAssets(config, problems);
|
|
22
|
+
checkDev(config, problems);
|
|
23
|
+
checkIsr(config, problems);
|
|
24
|
+
if (problems.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
throw new Error(`[ress] La configuración tiene ${problems.length === 1 ? 'un problema' : `${problems.length} problemas`}:\n` +
|
|
27
|
+
problems.map((p) => ` · ${p}`).join('\n'));
|
|
28
|
+
}
|
|
29
|
+
function checkBasePath({ basePath }, problems) {
|
|
30
|
+
if (basePath === undefined)
|
|
31
|
+
return;
|
|
32
|
+
if (typeof basePath !== 'string') {
|
|
33
|
+
return void problems.push('`basePath` tiene que ser una cadena.');
|
|
34
|
+
}
|
|
35
|
+
if (/[?#]/.test(basePath)) {
|
|
36
|
+
problems.push('`basePath` es un prefijo de ruta: no puede llevar `?` ni `#`.');
|
|
37
|
+
}
|
|
38
|
+
if (/^https?:\/\//i.test(basePath)) {
|
|
39
|
+
problems.push('`basePath` es un prefijo de ruta, no una URL completa. Para servir desde ' +
|
|
40
|
+
'otro dominio está `assets.cdnPrefix`.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function checkTrailingSlash({ trailingSlash }, problems) {
|
|
44
|
+
const valid = ['always', 'never', 'ignore'];
|
|
45
|
+
if (trailingSlash !== undefined && !valid.includes(trailingSlash)) {
|
|
46
|
+
problems.push(`\`trailingSlash\` sólo acepta ${valid.join(', ')}; se recibió "${trailingSlash}".`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function checkPublicEnv({ publicEnv }, problems) {
|
|
50
|
+
if (publicEnv === undefined)
|
|
51
|
+
return;
|
|
52
|
+
if (!Array.isArray(publicEnv)) {
|
|
53
|
+
return void problems.push('`publicEnv` tiene que ser una lista de nombres de variable.');
|
|
54
|
+
}
|
|
55
|
+
for (const name of publicEnv) {
|
|
56
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
57
|
+
problems.push('`publicEnv` sólo admite nombres de variable no vacíos.');
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Las tres son funciones para que el proyecto pueda calcularlas al arrancar. */
|
|
63
|
+
function checkCallable(config, problems) {
|
|
64
|
+
for (const key of ['redirects', 'rewrites', 'headers']) {
|
|
65
|
+
const value = config[key];
|
|
66
|
+
if (value !== undefined && typeof value !== 'function') {
|
|
67
|
+
problems.push(`\`${key}\` tiene que ser una función que devuelve la lista, no la lista. ` +
|
|
68
|
+
'Así el proyecto puede calcularla al arrancar.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function checkAssets({ assets }, problems) {
|
|
73
|
+
const prefix = assets?.cdnPrefix;
|
|
74
|
+
if (prefix === undefined)
|
|
75
|
+
return;
|
|
76
|
+
if (typeof prefix !== 'string' || !/^(https?:)?\/\//.test(prefix)) {
|
|
77
|
+
problems.push('`assets.cdnPrefix` tiene que ser una URL absoluta o empezar con `//`: ' +
|
|
78
|
+
'los artefactos se sirven desde otro origen.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function checkIsr({ isr }, problems) {
|
|
82
|
+
if (isr === undefined)
|
|
83
|
+
return;
|
|
84
|
+
if (typeof isr !== 'object' || isr === null) {
|
|
85
|
+
return void problems.push('`isr` tiene que ser un objeto.');
|
|
86
|
+
}
|
|
87
|
+
for (const key of ['maxEntries', 'maxBytes', 'generationTimeoutMs']) {
|
|
88
|
+
const value = isr[key];
|
|
89
|
+
if (value !== undefined && !(Number.isInteger(value) && value > 0)) {
|
|
90
|
+
problems.push(`\`isr.${key}\` tiene que ser un entero mayor que cero.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (isr.sharedCache !== undefined && !['auto', 'always', 'never'].includes(isr.sharedCache)) {
|
|
94
|
+
problems.push("`isr.sharedCache` sólo acepta 'auto', 'always' o 'never'.");
|
|
95
|
+
}
|
|
96
|
+
if (isr.dev !== undefined && typeof isr.dev !== 'boolean') {
|
|
97
|
+
problems.push('`isr.dev` tiene que ser un booleano.');
|
|
98
|
+
}
|
|
99
|
+
// Un secreto corto se adivina: la vista previa renderiza con cookies y
|
|
100
|
+
// credenciales, así que es una puerta hacia contenido no público.
|
|
101
|
+
if (isr.previewSecret !== undefined && (typeof isr.previewSecret !== 'string' || isr.previewSecret.length < 16)) {
|
|
102
|
+
problems.push('`isr.previewSecret` tiene que ser una cadena de al menos 16 caracteres.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `dev` ya no es una clave que el framework consulte: `dev.hmr` y
|
|
107
|
+
* `dev.prettyErrors` no tenían ningún efecto (R-12) y se retiraron. Declararla
|
|
108
|
+
* falla al arrancar en vez de quedar leída y descartada en silencio.
|
|
109
|
+
*/
|
|
110
|
+
function checkDev({ dev }, problems) {
|
|
111
|
+
if (dev === undefined)
|
|
112
|
+
return;
|
|
113
|
+
problems.push('`dev` ya no es una clave reconocida por el framework: `dev.hmr` y ' +
|
|
114
|
+
'`dev.prettyErrors` no tenían efecto y se retiraron. Sacá esta clave de ' +
|
|
115
|
+
'tu configuración.');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Comprueba las reglas que devolvieron `redirects`, `rewrites` y `headers`.
|
|
119
|
+
*
|
|
120
|
+
* Se hace después de invocarlas, no en `validateConfig`, porque hasta ese momento
|
|
121
|
+
* son funciones sin ejecutar.
|
|
122
|
+
*/
|
|
123
|
+
function validateRules(redirects, rewrites, headers) {
|
|
124
|
+
const problems = [];
|
|
125
|
+
for (const [index, rule] of redirects.entries()) {
|
|
126
|
+
if (!rule?.source || !rule?.destination) {
|
|
127
|
+
problems.push(`La redirección #${index + 1} necesita \`source\` y \`destination\`.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [index, rule] of rewrites.entries()) {
|
|
131
|
+
if (!rule?.source || !rule?.destination) {
|
|
132
|
+
problems.push(`La reescritura #${index + 1} necesita \`source\` y \`destination\`.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const [index, rule] of headers.entries()) {
|
|
136
|
+
if (!rule?.source || !Array.isArray(rule?.headers)) {
|
|
137
|
+
problems.push(`La regla de cabeceras #${index + 1} necesita \`source\` y \`headers\`.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (problems.length) {
|
|
141
|
+
throw new Error(`[ress] La configuración tiene reglas inválidas:\n` + problems.map((p) => ` · ${p}`).join('\n'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ResolvedConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Recarga de la configuración en desarrollo.
|
|
4
|
+
*
|
|
5
|
+
* Se observa la carpeta `config/`: cualquiera de sus capas, no sólo la del modo
|
|
6
|
+
* activo, porque `default` y `local` también participan de la fusión.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Claves que se pueden aplicar sin reiniciar.
|
|
10
|
+
*
|
|
11
|
+
* Todas se consultan al atender una petición, así que la siguiente ya ve el
|
|
12
|
+
* valor nuevo.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HOT_KEYS: readonly ["redirects", "rewrites", "headers", "trailingSlash"];
|
|
15
|
+
/**
|
|
16
|
+
* Claves que exigen reiniciar, y por qué.
|
|
17
|
+
*
|
|
18
|
+
* El framework lo dice nombrando la clave en vez de aplicar la mitad de la
|
|
19
|
+
* configuración: media configuración aplicada es un estado que nadie declaró y
|
|
20
|
+
* que produce errores que no se corresponden con ningún archivo.
|
|
21
|
+
*/
|
|
22
|
+
export declare const RESTART_REASONS: Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Claves que se leen al construir la aplicación de servidor de ress —el
|
|
25
|
+
* router, la caché incremental, los middlewares de la vía de escape—, no el
|
|
26
|
+
* servidor de Vite.
|
|
27
|
+
*
|
|
28
|
+
* Reiniciar Vite no las aplica: la CLI arma esa aplicación una sola vez, fuera
|
|
29
|
+
* de Vite. Para ellas no se reinicia nada ni se dice que se reinició: se pide
|
|
30
|
+
* detener y volver a arrancar `ress dev`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MANUAL_RESTART_KEYS: ReadonlySet<string>;
|
|
33
|
+
/** Qué claves de primer nivel cambiaron entre dos configuraciones resueltas. */
|
|
34
|
+
export declare function changedKeys(previous: ResolvedConfig, next: ResolvedConfig): string[];
|
|
35
|
+
/** Si alguna de las claves cambiadas exige volver a arrancar `ress dev` a mano. */
|
|
36
|
+
export declare function manualRestartReason(changed: readonly string[]): string | undefined;
|
|
37
|
+
/** El motivo del reinicio, o nada si todo lo que cambió se aplica en caliente. */
|
|
38
|
+
export declare function restartReason(changed: readonly string[]): string | undefined;
|
|
39
|
+
/** ¿Este archivo es una capa de configuración del proyecto? */
|
|
40
|
+
export declare function isConfigLayer(file: string, root: string): boolean;
|
|
41
|
+
export interface WatchConfigOptions {
|
|
42
|
+
root: string;
|
|
43
|
+
mode: string;
|
|
44
|
+
/** La configuración vigente, para comparar contra la nueva. */
|
|
45
|
+
current: () => ResolvedConfig;
|
|
46
|
+
/** Qué hacer con la configuración nueva cuando se puede aplicar en caliente. */
|
|
47
|
+
onReload: (config: ResolvedConfig) => void;
|
|
48
|
+
/** Qué hacer cuando hace falta reiniciar. Por defecto, pedírselo a Vite. */
|
|
49
|
+
onRestart?: (reason: string) => void;
|
|
50
|
+
log?: (message: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/** Conecta el observador de Vite con la carpeta de configuración. */
|
|
53
|
+
export declare function watchConfig(server: any, options: WatchConfigOptions): void;
|