@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,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfig = loadConfig;
|
|
4
|
+
exports.normalizeBasePath = normalizeBasePath;
|
|
5
|
+
const config_1 = require("@ressjs/config");
|
|
6
|
+
const validate_1 = require("./validate");
|
|
7
|
+
/**
|
|
8
|
+
* Lee la configuración del proyecto y la deja lista para consultar.
|
|
9
|
+
*
|
|
10
|
+
* **Una sola superficie**: la carpeta `config/`, con un archivo por modo.
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* config/default.js ← lo común, opcional
|
|
14
|
+
* config/development.js
|
|
15
|
+
* config/production.js
|
|
16
|
+
* config/test.js
|
|
17
|
+
* config/local.js ← esta máquina; no se sube
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Cada archivo exporta por defecto un objeto con **todo**: `basePath`,
|
|
21
|
+
* `redirects`, `publicEnv`, `dev.hmr`, `security`, y también los valores propios
|
|
22
|
+
* del proyecto —la URL de una API, una clave—. Se fusionan `default` → el modo
|
|
23
|
+
* activo → `local`, y la última capa que declara una clave gana.
|
|
24
|
+
*
|
|
25
|
+
* Antes esto estaban partido en dos archivos distintos, `ress.config` para el
|
|
26
|
+
* comportamiento y `config/` para los valores. Eran dos lugares donde buscar la
|
|
27
|
+
* misma respuesta —"¿de dónde sale este valor?"— y dos formas de declararla.
|
|
28
|
+
*/
|
|
29
|
+
async function loadConfig(root = process.cwd(), mode = process.env.NODE_ENV === 'production' ? 'production' : 'development') {
|
|
30
|
+
// La misma resolución que lee el código de la aplicación con
|
|
31
|
+
// `import { config } from '@ressjs/config'`: un solo objeto, resuelto una vez.
|
|
32
|
+
const declared = (await (0, config_1.resolveConfig)(root, mode));
|
|
33
|
+
(0, validate_1.validateConfig)(declared);
|
|
34
|
+
return resolve(declared);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Completa lo que el proyecto no declaró.
|
|
38
|
+
*
|
|
39
|
+
* Se hace una vez al arrancar y no por consulta: así el resto del framework lee
|
|
40
|
+
* `config.dev.hmr` sin `?.` ni `??` en cada uso, y los valores por defecto están
|
|
41
|
+
* escritos en un solo lugar.
|
|
42
|
+
*/
|
|
43
|
+
async function resolve(declared) {
|
|
44
|
+
return {
|
|
45
|
+
// Sin barra final: se agrega al componer, y tenerla en el valor guardado
|
|
46
|
+
// obliga a recordar si ya está en cada concatenación.
|
|
47
|
+
basePath: normalizeBasePath(declared.basePath ?? ''),
|
|
48
|
+
trailingSlash: declared.trailingSlash ?? 'ignore',
|
|
49
|
+
// Las tres son funciones para que el proyecto pueda leer un archivo o
|
|
50
|
+
// consultar algo al arrancar. Se invocan una vez, acá.
|
|
51
|
+
redirects: (await declared.redirects?.()) ?? [],
|
|
52
|
+
rewrites: (await declared.rewrites?.()) ?? [],
|
|
53
|
+
headers: (await declared.headers?.()) ?? [],
|
|
54
|
+
publicEnv: declared.publicEnv ?? [],
|
|
55
|
+
extensions: declared.extensions ?? {},
|
|
56
|
+
platform: declared.platform ?? {},
|
|
57
|
+
security: declared.security ?? {},
|
|
58
|
+
assets: declared.assets ?? {},
|
|
59
|
+
dev: {
|
|
60
|
+
prettyErrors: declared.dev?.prettyErrors ?? true,
|
|
61
|
+
hmr: declared.dev?.hmr ?? true,
|
|
62
|
+
},
|
|
63
|
+
server: declared.server ?? {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** `'/app/'` y `'app'` describen lo mismo: se guardan como `'/app'`. */
|
|
67
|
+
function normalizeBasePath(basePath) {
|
|
68
|
+
const trimmed = basePath.trim();
|
|
69
|
+
if (!trimmed || trimmed === '/')
|
|
70
|
+
return '';
|
|
71
|
+
const withLeading = trimmed.startsWith('/') ? trimmed : '/' + trimmed;
|
|
72
|
+
return withLeading.replace(/\/+$/, '');
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type CompiledRules } from './rules';
|
|
2
|
+
import type { ResolvedConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Lo que la configuración aporta al pipeline de petición.
|
|
5
|
+
*
|
|
6
|
+
* El orden es fijo y está en el plan:
|
|
7
|
+
*
|
|
8
|
+
* 1. barra final — para que todo lo que sigue vea una sola forma de cada URL
|
|
9
|
+
* 2. prefijo de ruta — para que el manifest trabaje siempre sin prefijo
|
|
10
|
+
* 3. redirecciones — si alguna coincide, se responde y se termina
|
|
11
|
+
* 4. reescrituras — cambian la ruta a resolver, no la que el navegador muestra
|
|
12
|
+
* 5. cabeceras — al escribir la respuesta, sin pisar las del framework
|
|
13
|
+
*/
|
|
14
|
+
export interface ConfigMiddlewareOptions {
|
|
15
|
+
/** Reglas ya compiladas. Se compilan una vez al arrancar, no por petición. */
|
|
16
|
+
compiled?: CompiledRules;
|
|
17
|
+
}
|
|
18
|
+
export declare function createConfigMiddleware(config: ResolvedConfig, options?: ConfigMiddlewareOptions): (req: any, res: any, next: (err?: unknown) => void) => void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createConfigMiddleware = createConfigMiddleware;
|
|
4
|
+
const base_path_1 = require("./base-path");
|
|
5
|
+
const rules_1 = require("./rules");
|
|
6
|
+
function createConfigMiddleware(config, options = {}) {
|
|
7
|
+
const rules = options.compiled ?? (0, rules_1.compileRules)(config);
|
|
8
|
+
const { basePath, trailingSlash } = config;
|
|
9
|
+
return function configMiddleware(req, res, next) {
|
|
10
|
+
const url = req.url ?? '/';
|
|
11
|
+
const cut = url.search(/[?#]/);
|
|
12
|
+
const pathname = cut === -1 ? url : url.slice(0, cut);
|
|
13
|
+
const suffix = cut === -1 ? '' : url.slice(cut);
|
|
14
|
+
const canonical = (0, base_path_1.canonicalTrailingSlash)(pathname, trailingSlash);
|
|
15
|
+
if (canonical !== undefined) {
|
|
16
|
+
// 308: el navegador y los intermediarios tienen que conservar el método.
|
|
17
|
+
// Un 301 convierte un POST en GET al repetir.
|
|
18
|
+
return redirect(res, 308, canonical + suffix);
|
|
19
|
+
}
|
|
20
|
+
const inner = (0, base_path_1.stripBasePath)(pathname, basePath);
|
|
21
|
+
if (inner === undefined) {
|
|
22
|
+
// Fuera del prefijo no hay aplicación: se marca para que ni los artefactos
|
|
23
|
+
// ni las páginas la atiendan. Sin la marca, declarar un prefijo dejaba el
|
|
24
|
+
// sitio servido por dos direcciones distintas a la vez, que es justo lo
|
|
25
|
+
// que un prefijo existe para evitar.
|
|
26
|
+
req.ressOutsideBasePath = true;
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
// La URL original queda a mano: los mensajes de error y los enlaces
|
|
30
|
+
// canónicos hablan de lo que el navegador pidió, no de lo que quedó
|
|
31
|
+
// después de quitar el prefijo.
|
|
32
|
+
req.ressOriginalUrl ??= url;
|
|
33
|
+
req.ressBasePath = basePath;
|
|
34
|
+
req.url = inner + suffix;
|
|
35
|
+
const redirection = (0, rules_1.matchRule)(rules.redirects, inner);
|
|
36
|
+
if (redirection) {
|
|
37
|
+
const target = (0, rules_1.applyParams)(redirection.compiled.rule.destination, redirection.params);
|
|
38
|
+
return redirect(res, redirection.compiled.rule.permanent ? 308 : 307, absolute(target) ? target : (0, base_path_1.withBasePath)(target, basePath) + suffix);
|
|
39
|
+
}
|
|
40
|
+
const rewrite = (0, rules_1.matchRule)(rules.rewrites, inner);
|
|
41
|
+
if (rewrite) {
|
|
42
|
+
// Sólo cambia lo que el servidor resuelve: `req.url` es interno, y la
|
|
43
|
+
// barra de direcciones del navegador no se entera.
|
|
44
|
+
req.url = (0, rules_1.applyParams)(rewrite.compiled.rule.destination, rewrite.params) + suffix;
|
|
45
|
+
}
|
|
46
|
+
applyHeaderRules(rules, inner, res);
|
|
47
|
+
next();
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function absolute(target) {
|
|
51
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith('//');
|
|
52
|
+
}
|
|
53
|
+
function redirect(res, status, location) {
|
|
54
|
+
res.statusCode = status;
|
|
55
|
+
res.setHeader('Location', location);
|
|
56
|
+
res.end();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Las cabeceras declaradas se escriben al enviar, no ahora.
|
|
60
|
+
*
|
|
61
|
+
* Ponerlas ya las dejaría expuestas a que el framework las pisara después; y
|
|
62
|
+
* ponerlas después de las suyas es justamente la regla: complementan lo que el
|
|
63
|
+
* framework decidió, no lo corrigen. Por eso sólo se escribe lo que nadie fijó.
|
|
64
|
+
*/
|
|
65
|
+
function applyHeaderRules(rules, pathname, res) {
|
|
66
|
+
const match = (0, rules_1.matchRule)(rules.headers, pathname);
|
|
67
|
+
if (!match)
|
|
68
|
+
return;
|
|
69
|
+
const write = res.writeHead?.bind(res);
|
|
70
|
+
if (!write)
|
|
71
|
+
return;
|
|
72
|
+
res.writeHead = (...args) => {
|
|
73
|
+
for (const { key, value } of match.compiled.rule.headers) {
|
|
74
|
+
if (res.getHeader?.(key) === undefined)
|
|
75
|
+
res.setHeader(key, value);
|
|
76
|
+
}
|
|
77
|
+
return write(...args);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/** Traslada los parámetros que capturó el origen al destino. */
|
|
48
|
+
export declare function applyParams(destination: string, params: Record<string, string | string[]>): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
/** Traslada los parámetros que capturó el origen al destino. */
|
|
142
|
+
function applyParams(destination, params) {
|
|
143
|
+
return destination.replace(/:(\.{3})?([A-Za-z0-9_]+)/g, (whole, _spread, name) => {
|
|
144
|
+
const value = params[name];
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
return whole;
|
|
147
|
+
return Array.isArray(value) ? value.join('/') : value;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
* dev: { hmr: false },
|
|
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, pero
|
|
50
|
+
* todavía no llega al pipeline de petición: `request-handler.ts` sigue
|
|
51
|
+
* leyendo `RouterOptions.security`/`securityOptions`, que es un objeto
|
|
52
|
+
* aparte. Declarar esta clave en `config/*.js` no tiene efecto todavía.
|
|
53
|
+
* F-019.
|
|
54
|
+
*/
|
|
55
|
+
security?: SecurityOptions;
|
|
56
|
+
/** Prefijo de CDN para archivos estáticos. Consumido por F-006. */
|
|
57
|
+
assets?: AssetsConfig;
|
|
58
|
+
/** Experiencia de desarrollo. Consumido por F-034 y F-015/F-016. */
|
|
59
|
+
dev?: DevConfig;
|
|
60
|
+
/**
|
|
61
|
+
* Vía de escape nivel 1 (F-033): extender la aplicación de servidor sin
|
|
62
|
+
* salir de los comandos de la CLI.
|
|
63
|
+
*/
|
|
64
|
+
server?: RessServerConfig;
|
|
65
|
+
/**
|
|
66
|
+
* Capas por modo, sólo en la forma de archivo único (`config.js` en vez de la
|
|
67
|
+
* carpeta). La forma de carpeta las expresa con un archivo por modo, que es la
|
|
68
|
+
* que conviene cuando la configuración crece.
|
|
69
|
+
*/
|
|
70
|
+
development?: ModeOverrides;
|
|
71
|
+
production?: ModeOverrides;
|
|
72
|
+
local?: ModeOverrides;
|
|
73
|
+
/**
|
|
74
|
+
* Los valores propios del proyecto: una URL de API, una clave, un feature
|
|
75
|
+
* flag. Conviven con las claves del framework en el mismo objeto y se leen con
|
|
76
|
+
* `import { config } from '@ressjs/config'`.
|
|
77
|
+
*/
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
/** Una capa de modo no puede contener otras capas. */
|
|
81
|
+
export type ModeOverrides = Omit<RessConfig, 'development' | 'production' | 'local'>;
|
|
82
|
+
export interface AssetsConfig {
|
|
83
|
+
/** Prefijo absoluto desde el que se sirven los artefactos. */
|
|
84
|
+
cdnPrefix?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface DevConfig {
|
|
87
|
+
/** Página de error con contexto en desarrollo. F-034. */
|
|
88
|
+
prettyErrors?: boolean;
|
|
89
|
+
/** Recarga en caliente. F-015 y F-016. */
|
|
90
|
+
hmr?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Vía de escape nivel 1: extender la aplicación de servidor desde la
|
|
94
|
+
* configuración, sin dejar de usar `ress dev` / `ress build` / `ress start`.
|
|
95
|
+
*
|
|
96
|
+
* Las funciones reciben el `Express` que construye `createRessApp` (F-033).
|
|
97
|
+
* `beforeRouter` corre antes de montar el enrutador del framework y gana ante
|
|
98
|
+
* sus rutas; `afterRouter` corre después y no puede pisarlas.
|
|
99
|
+
*/
|
|
100
|
+
export interface RessServerConfig {
|
|
101
|
+
/** Se ejecuta antes de montar el enrutador del framework. Gana ante rutas del framework. */
|
|
102
|
+
beforeRouter?: (app: import('express').Express) => void | Promise<void>;
|
|
103
|
+
/** Se ejecuta después. No puede pisar rutas del framework. */
|
|
104
|
+
afterRouter?: (app: import('express').Express) => void | Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Tokens de plataforma propios del proyecto, para el autocompletado.
|
|
108
|
+
*
|
|
109
|
+
* `detect`, `axisPrecedence` y `edge` no están acá: el motor de detección los
|
|
110
|
+
* lee de `defaultPlatformRegistry`, no de esta configuración. Declararlos aquí
|
|
111
|
+
* sin que nada los consuma es la clase de clave que promete algo que no hace.
|
|
112
|
+
*/
|
|
113
|
+
export interface PlatformConfig {
|
|
114
|
+
values?: Array<{
|
|
115
|
+
token: string;
|
|
116
|
+
axis: string;
|
|
117
|
+
label?: string;
|
|
118
|
+
}>;
|
|
119
|
+
/** Plataformas para las que se prepara el proyecto generado. F-033. */
|
|
120
|
+
targets?: string[];
|
|
121
|
+
/**
|
|
122
|
+
* Intención declarativa de usar clientes completos forkeables. Las plantillas
|
|
123
|
+
* y sus runtimes pertenecen a F-028 (React Native) y F-029 (Electron).
|
|
124
|
+
*/
|
|
125
|
+
forkedClients?: string[];
|
|
126
|
+
}
|
|
127
|
+
export interface RedirectRule {
|
|
128
|
+
source: string;
|
|
129
|
+
destination: string;
|
|
130
|
+
/** Permanente emite 308; temporal, 307. */
|
|
131
|
+
permanent?: boolean;
|
|
132
|
+
}
|
|
133
|
+
export interface RewriteRule {
|
|
134
|
+
source: string;
|
|
135
|
+
destination: string;
|
|
136
|
+
}
|
|
137
|
+
export interface HeaderRule {
|
|
138
|
+
source: string;
|
|
139
|
+
headers: Array<{
|
|
140
|
+
key: string;
|
|
141
|
+
value: string;
|
|
142
|
+
}>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* La configuración ya resuelta.
|
|
146
|
+
*
|
|
147
|
+
* Todas las claves que el framework consulta existen, así que nadie tiene que
|
|
148
|
+
* escribir `config.dev?.hmr ?? true` en cada uso. Las capas de modo desaparecen:
|
|
149
|
+
* ya se fusionaron.
|
|
150
|
+
*/
|
|
151
|
+
export interface ResolvedConfig {
|
|
152
|
+
basePath: string;
|
|
153
|
+
trailingSlash: TrailingSlashMode;
|
|
154
|
+
redirects: RedirectRule[];
|
|
155
|
+
rewrites: RewriteRule[];
|
|
156
|
+
headers: HeaderRule[];
|
|
157
|
+
publicEnv: string[];
|
|
158
|
+
extensions: Partial<ExtensionPolicy>;
|
|
159
|
+
platform: PlatformConfig;
|
|
160
|
+
security: SecurityOptions;
|
|
161
|
+
assets: Required<AssetsConfig> | AssetsConfig;
|
|
162
|
+
dev: Required<DevConfig>;
|
|
163
|
+
server: RessServerConfig;
|
|
164
|
+
}
|
|
@@ -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,105 @@
|
|
|
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
|
+
if (problems.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
throw new Error(`[ress] La configuración tiene ${problems.length === 1 ? 'un problema' : `${problems.length} problemas`}:\n` +
|
|
25
|
+
problems.map((p) => ` · ${p}`).join('\n'));
|
|
26
|
+
}
|
|
27
|
+
function checkBasePath({ basePath }, problems) {
|
|
28
|
+
if (basePath === undefined)
|
|
29
|
+
return;
|
|
30
|
+
if (typeof basePath !== 'string') {
|
|
31
|
+
return void problems.push('`basePath` tiene que ser una cadena.');
|
|
32
|
+
}
|
|
33
|
+
if (/[?#]/.test(basePath)) {
|
|
34
|
+
problems.push('`basePath` es un prefijo de ruta: no puede llevar `?` ni `#`.');
|
|
35
|
+
}
|
|
36
|
+
if (/^https?:\/\//i.test(basePath)) {
|
|
37
|
+
problems.push('`basePath` es un prefijo de ruta, no una URL completa. Para servir desde ' +
|
|
38
|
+
'otro dominio está `assets.cdnPrefix`.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function checkTrailingSlash({ trailingSlash }, problems) {
|
|
42
|
+
const valid = ['always', 'never', 'ignore'];
|
|
43
|
+
if (trailingSlash !== undefined && !valid.includes(trailingSlash)) {
|
|
44
|
+
problems.push(`\`trailingSlash\` sólo acepta ${valid.join(', ')}; se recibió "${trailingSlash}".`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function checkPublicEnv({ publicEnv }, problems) {
|
|
48
|
+
if (publicEnv === undefined)
|
|
49
|
+
return;
|
|
50
|
+
if (!Array.isArray(publicEnv)) {
|
|
51
|
+
return void problems.push('`publicEnv` tiene que ser una lista de nombres de variable.');
|
|
52
|
+
}
|
|
53
|
+
for (const name of publicEnv) {
|
|
54
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
55
|
+
problems.push('`publicEnv` sólo admite nombres de variable no vacíos.');
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Las tres son funciones para que el proyecto pueda calcularlas al arrancar. */
|
|
61
|
+
function checkCallable(config, problems) {
|
|
62
|
+
for (const key of ['redirects', 'rewrites', 'headers']) {
|
|
63
|
+
const value = config[key];
|
|
64
|
+
if (value !== undefined && typeof value !== 'function') {
|
|
65
|
+
problems.push(`\`${key}\` tiene que ser una función que devuelve la lista, no la lista. ` +
|
|
66
|
+
'Así el proyecto puede calcularla al arrancar.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function checkAssets({ assets }, problems) {
|
|
71
|
+
const prefix = assets?.cdnPrefix;
|
|
72
|
+
if (prefix === undefined)
|
|
73
|
+
return;
|
|
74
|
+
if (typeof prefix !== 'string' || !/^(https?:)?\/\//.test(prefix)) {
|
|
75
|
+
problems.push('`assets.cdnPrefix` tiene que ser una URL absoluta o empezar con `//`: ' +
|
|
76
|
+
'los artefactos se sirven desde otro origen.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Comprueba las reglas que devolvieron `redirects`, `rewrites` y `headers`.
|
|
81
|
+
*
|
|
82
|
+
* Se hace después de invocarlas, no en `validateConfig`, porque hasta ese momento
|
|
83
|
+
* son funciones sin ejecutar.
|
|
84
|
+
*/
|
|
85
|
+
function validateRules(redirects, rewrites, headers) {
|
|
86
|
+
const problems = [];
|
|
87
|
+
for (const [index, rule] of redirects.entries()) {
|
|
88
|
+
if (!rule?.source || !rule?.destination) {
|
|
89
|
+
problems.push(`La redirección #${index + 1} necesita \`source\` y \`destination\`.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [index, rule] of rewrites.entries()) {
|
|
93
|
+
if (!rule?.source || !rule?.destination) {
|
|
94
|
+
problems.push(`La reescritura #${index + 1} necesita \`source\` y \`destination\`.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [index, rule] of headers.entries()) {
|
|
98
|
+
if (!rule?.source || !Array.isArray(rule?.headers)) {
|
|
99
|
+
problems.push(`La regla de cabeceras #${index + 1} necesita \`source\` y \`headers\`.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (problems.length) {
|
|
103
|
+
throw new Error(`[ress] La configuración tiene reglas inválidas:\n` + problems.map((p) => ` · ${p}`).join('\n'));
|
|
104
|
+
}
|
|
105
|
+
}
|