@ressjs/vite-router 0.5.0 → 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 +41 -31
- 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 -103
- 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,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRoutePattern = parseRoutePattern;
|
|
4
|
+
exports.pageIdFromFile = pageIdFromFile;
|
|
5
|
+
exports.classifyEntry = classifyEntry;
|
|
6
|
+
const EXTENSION = /\.(tsx|ts|jsx|js|mts|mjs)$/;
|
|
7
|
+
function parseRoutePattern(sourceFile, pagesDir = 'app/pages') {
|
|
8
|
+
const relative = stripPagesDir(sourceFile, pagesDir).replace(EXTENSION, '');
|
|
9
|
+
const segments = [];
|
|
10
|
+
const paramNames = [];
|
|
11
|
+
const groups = [];
|
|
12
|
+
const parts = relative.split('/').filter(Boolean);
|
|
13
|
+
for (const [index, raw] of parts.entries()) {
|
|
14
|
+
// Un grupo organiza archivos sin aparecer en la URL.
|
|
15
|
+
if (raw.startsWith('(') && raw.endsWith(')')) {
|
|
16
|
+
groups.push(raw.slice(1, -1));
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
// `index` nombra la ruta de su carpeta, así que no aporta segmento propio.
|
|
20
|
+
if (raw === 'index' && index === parts.length - 1)
|
|
21
|
+
continue;
|
|
22
|
+
const segment = parseSegment(raw, sourceFile);
|
|
23
|
+
// Todo lo que siga a un catch-all sería inalcanzable: el catch-all ya
|
|
24
|
+
// consumió el resto de la URL.
|
|
25
|
+
const previous = segments[segments.length - 1];
|
|
26
|
+
if (previous && (previous.kind === 'catchAll' || previous.kind === 'optionalCatchAll')) {
|
|
27
|
+
throw new Error(`[ress] "${sourceFile}" pone un segmento después de "[...${previous.value}]". ` +
|
|
28
|
+
'Un catch-all consume el resto de la URL, así que nada que lo siga puede alcanzarse.');
|
|
29
|
+
}
|
|
30
|
+
if (segment.kind !== 'static') {
|
|
31
|
+
if (paramNames.includes(segment.value)) {
|
|
32
|
+
throw new Error(`[ress] "${sourceFile}" usa el parámetro "${segment.value}" dos veces. ` +
|
|
33
|
+
'Cada parámetro tiene que tener un nombre distinto para poder leerse.');
|
|
34
|
+
}
|
|
35
|
+
paramNames.push(segment.value);
|
|
36
|
+
}
|
|
37
|
+
segments.push(segment);
|
|
38
|
+
}
|
|
39
|
+
return { pattern: toPattern(segments), segments, paramNames, groups };
|
|
40
|
+
}
|
|
41
|
+
function parseSegment(raw, sourceFile) {
|
|
42
|
+
const open = raw.indexOf('[');
|
|
43
|
+
const close = raw.lastIndexOf(']');
|
|
44
|
+
if (open === -1 && close === -1)
|
|
45
|
+
return { kind: 'static', value: raw };
|
|
46
|
+
if (open !== 0 || close !== raw.length - 1) {
|
|
47
|
+
throw new Error(`[ress] "${sourceFile}" tiene corchetes desbalanceados o mezclados con texto en "${raw}". ` +
|
|
48
|
+
'Un segmento con parámetro se escribe entero entre corchetes: [id], [...slug], [[...slug]].');
|
|
49
|
+
}
|
|
50
|
+
const inner = raw.slice(1, -1);
|
|
51
|
+
// `[[...slug]]` — el doble corchete lo vuelve opcional.
|
|
52
|
+
if (inner.startsWith('[') && inner.endsWith(']')) {
|
|
53
|
+
const name = requireName(inner.slice(1, -1).replace(/^\.\.\./, ''), raw, sourceFile);
|
|
54
|
+
if (!inner.slice(1, -1).startsWith('...')) {
|
|
55
|
+
throw new Error(`[ress] "${sourceFile}" escribe "${raw}" con doble corchete pero sin "...". ` +
|
|
56
|
+
'El doble corchete es para un catch-all opcional: [[...slug]].');
|
|
57
|
+
}
|
|
58
|
+
return { kind: 'optionalCatchAll', value: name };
|
|
59
|
+
}
|
|
60
|
+
if (inner.startsWith('...')) {
|
|
61
|
+
return { kind: 'catchAll', value: requireName(inner.slice(3), raw, sourceFile) };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'dynamic', value: requireName(inner, raw, sourceFile) };
|
|
64
|
+
}
|
|
65
|
+
function requireName(name, raw, sourceFile) {
|
|
66
|
+
if (!name) {
|
|
67
|
+
throw new Error(`[ress] "${sourceFile}" declara el parámetro "${raw}" sin nombre. ` +
|
|
68
|
+
'Sin nombre no hay forma de leer su valor desde la página.');
|
|
69
|
+
}
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
function toPattern(segments) {
|
|
73
|
+
if (segments.length === 0)
|
|
74
|
+
return '/';
|
|
75
|
+
return ('/' +
|
|
76
|
+
segments
|
|
77
|
+
.map((s) => {
|
|
78
|
+
if (s.kind === 'static')
|
|
79
|
+
return s.value;
|
|
80
|
+
if (s.kind === 'dynamic')
|
|
81
|
+
return `[${s.value}]`;
|
|
82
|
+
if (s.kind === 'catchAll')
|
|
83
|
+
return `[...${s.value}]`;
|
|
84
|
+
return `[[...${s.value}]]`;
|
|
85
|
+
})
|
|
86
|
+
.join('/'));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Identificador estable de una página.
|
|
90
|
+
*
|
|
91
|
+
* Es la misma clave que usa el catálogo de variantes de F-016, para que las dos
|
|
92
|
+
* estructuras hablen del mismo objeto sin traducir nada entre ellas.
|
|
93
|
+
*/
|
|
94
|
+
function pageIdFromFile(sourceFile, pagesDir = 'app/pages') {
|
|
95
|
+
return stripPagesDir(sourceFile, pagesDir)
|
|
96
|
+
.replace(EXTENSION, '')
|
|
97
|
+
.replace(/\//g, '__')
|
|
98
|
+
.replace(/\[([^\]]+)\]/g, '_$1_');
|
|
99
|
+
}
|
|
100
|
+
/** Qué clase de entrada es, según dónde vive el archivo. */
|
|
101
|
+
function classifyEntry(sourceFile, pagesDir = 'app/pages') {
|
|
102
|
+
const relative = stripPagesDir(sourceFile, pagesDir);
|
|
103
|
+
if (relative.startsWith('api/'))
|
|
104
|
+
return 'api';
|
|
105
|
+
if (/(^|\/)_error(\.|\/)|(^|\/)error\//.test(relative))
|
|
106
|
+
return 'error';
|
|
107
|
+
return 'page';
|
|
108
|
+
}
|
|
109
|
+
function stripPagesDir(sourceFile, pagesDir) {
|
|
110
|
+
const normalized = sourceFile.replace(/\\/g, '/');
|
|
111
|
+
const marker = pagesDir.replace(/\\/g, '/').replace(/\/$/, '') + '/';
|
|
112
|
+
const index = normalized.indexOf(marker);
|
|
113
|
+
return index === -1 ? normalized.replace(/^\//, '') : normalized.slice(index + marker.length);
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RouteEntry, RouteSegment } from './types';
|
|
2
|
+
/** Clave de comparación de una ruta: la especificidad de cada segmento, en orden. */
|
|
3
|
+
export declare function computeRank(segments: readonly RouteSegment[]): number[];
|
|
4
|
+
/**
|
|
5
|
+
* Orden en que se prueban las rutas.
|
|
6
|
+
*
|
|
7
|
+
* Se compara segmento a segmento; el primero que difiere decide. A igual
|
|
8
|
+
* especificidad gana la más larga —describe más de la URL— y, si aún empatan, el
|
|
9
|
+
* orden alfabético del patrón, para que el resultado no dependa de en qué orden
|
|
10
|
+
* el sistema de archivos devolvió los nombres.
|
|
11
|
+
*/
|
|
12
|
+
export declare function compareEntries(a: RouteEntry, b: RouteEntry): number;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeRank = computeRank;
|
|
4
|
+
exports.compareEntries = compareEntries;
|
|
5
|
+
/**
|
|
6
|
+
* Cuán específico es cada tipo de segmento. Menor gana.
|
|
7
|
+
*
|
|
8
|
+
* Un literal describe una sola URL; un parámetro, todas las de su posición; un
|
|
9
|
+
* catch-all, todas las que empiecen igual. Ordenar por esto es lo que hace que
|
|
10
|
+
* `/users/nuevo` gane sobre `/users/[id]` sin que nadie declare la prioridad.
|
|
11
|
+
*/
|
|
12
|
+
const KIND_RANK = {
|
|
13
|
+
static: 0,
|
|
14
|
+
dynamic: 1,
|
|
15
|
+
catchAll: 2,
|
|
16
|
+
optionalCatchAll: 3,
|
|
17
|
+
};
|
|
18
|
+
/** Clave de comparación de una ruta: la especificidad de cada segmento, en orden. */
|
|
19
|
+
function computeRank(segments) {
|
|
20
|
+
return segments.map((s) => KIND_RANK[s.kind]);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Orden en que se prueban las rutas.
|
|
24
|
+
*
|
|
25
|
+
* Se compara segmento a segmento; el primero que difiere decide. A igual
|
|
26
|
+
* especificidad gana la más larga —describe más de la URL— y, si aún empatan, el
|
|
27
|
+
* orden alfabético del patrón, para que el resultado no dependa de en qué orden
|
|
28
|
+
* el sistema de archivos devolvió los nombres.
|
|
29
|
+
*/
|
|
30
|
+
function compareEntries(a, b) {
|
|
31
|
+
const length = Math.min(a.rank.length, b.rank.length);
|
|
32
|
+
for (let i = 0; i < length; i++) {
|
|
33
|
+
if (a.rank[i] !== b.rank[i])
|
|
34
|
+
return a.rank[i] - b.rank[i];
|
|
35
|
+
}
|
|
36
|
+
if (a.rank.length !== b.rank.length) {
|
|
37
|
+
const [corta, larga] = a.rank.length < b.rank.length ? [a, b] : [b, a];
|
|
38
|
+
const extra = larga.rank[corta.rank.length];
|
|
39
|
+
// Un catch-all opcional puede no consumir nada, así que la ruta que lo lleva
|
|
40
|
+
// atiende también todo lo que atiende la corta: es la más general de las dos.
|
|
41
|
+
// Sin esto, `/users/[id]/[[...rest]]` se probaría antes que `/users/[id]` y
|
|
42
|
+
// esta última quedaría inalcanzable.
|
|
43
|
+
if (extra === KIND_RANK.optionalCatchAll)
|
|
44
|
+
return corta === a ? -1 : 1;
|
|
45
|
+
// En cualquier otro caso, la más larga describe más de la URL.
|
|
46
|
+
return b.rank.length - a.rank.length;
|
|
47
|
+
}
|
|
48
|
+
return a.pattern.localeCompare(b.pattern);
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Router, type RequestHandler } from 'express';
|
|
2
|
+
import type { ModuleLoader } from '../runtime/module-loader';
|
|
3
|
+
import type { RouteEntry, RouteManifest } from './types';
|
|
4
|
+
export interface ChainContext {
|
|
5
|
+
moduleLoader?: ModuleLoader;
|
|
6
|
+
renderOptions: {
|
|
7
|
+
enablePlatformDetection: boolean;
|
|
8
|
+
renderFunction?: (options: {
|
|
9
|
+
url: string;
|
|
10
|
+
platform: string;
|
|
11
|
+
req: any;
|
|
12
|
+
}) => Promise<string>;
|
|
13
|
+
viteConfig: unknown;
|
|
14
|
+
security: unknown;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* La cadena de middlewares de una entrada.
|
|
19
|
+
*
|
|
20
|
+
* Es el cuerpo que estaba duplicado entre los dos constructores de router del
|
|
21
|
+
* modelo anterior: lo que hace un middleware de carpeta, `getServerSideProps` y
|
|
22
|
+
* el render, en ese orden. Tenerlo una vez es lo que garantiza que los dos
|
|
23
|
+
* caminos se comporten igual.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildChain(entry: RouteEntry, ctx: ChainContext): Promise<RequestHandler[]>;
|
|
26
|
+
export interface RessRouterState {
|
|
27
|
+
manifest: RouteManifest;
|
|
28
|
+
chains: Map<string, RequestHandler[]>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Construye el estado de ruteo: el manifest más la cadena de cada entrada.
|
|
32
|
+
*
|
|
33
|
+
* Se arma entero y se devuelve entero, de modo que reemplazarlo en desarrollo
|
|
34
|
+
* sea una sola asignación y ninguna petición vea una mezcla de dos versiones.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildRouterState(manifest: RouteManifest, ctx: ChainContext): Promise<RessRouterState>;
|
|
37
|
+
/**
|
|
38
|
+
* El router de páginas.
|
|
39
|
+
*
|
|
40
|
+
* Un solo despachador para todas las rutas y un solo manejador de errores, que
|
|
41
|
+
* sabe qué ruta falló porque el despachador lo dejó en la petición. El modelo
|
|
42
|
+
* anterior registraba un manejador por página.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createPageRouter(getState: () => RessRouterState, ctx: ChainContext): Router;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildChain = buildChain;
|
|
4
|
+
exports.buildRouterState = buildRouterState;
|
|
5
|
+
exports.createPageRouter = createPageRouter;
|
|
6
|
+
const express_1 = require("express");
|
|
7
|
+
const middlewares_1 = require("../helpers/middlewares");
|
|
8
|
+
const render_1 = require("../render");
|
|
9
|
+
const dispatcher_1 = require("./dispatcher");
|
|
10
|
+
/**
|
|
11
|
+
* La cadena de middlewares de una entrada.
|
|
12
|
+
*
|
|
13
|
+
* Es el cuerpo que estaba duplicado entre los dos constructores de router del
|
|
14
|
+
* modelo anterior: lo que hace un middleware de carpeta, `getServerSideProps` y
|
|
15
|
+
* el render, en ese orden. Tenerlo una vez es lo que garantiza que los dos
|
|
16
|
+
* caminos se comporten igual.
|
|
17
|
+
*/
|
|
18
|
+
async function buildChain(entry, ctx) {
|
|
19
|
+
return [
|
|
20
|
+
// Express reasigna `req.params` cada vez que la petición entra a un router
|
|
21
|
+
// anidado, así que los valores que extrajo el despachador se restauran acá,
|
|
22
|
+
// ya dentro de la cadena de la página.
|
|
23
|
+
(req, _res, next) => {
|
|
24
|
+
if (req.ressParams)
|
|
25
|
+
req.params = req.ressParams;
|
|
26
|
+
next();
|
|
27
|
+
},
|
|
28
|
+
// Se busca por la ubicación del archivo y no por el patrón: un grupo no
|
|
29
|
+
// aparece en la URL, así que `app/pages/(marketing)/about.tsx` tiene patrón
|
|
30
|
+
// `/about` y sus middlewares viven en `/(marketing)`. Compararlos daría
|
|
31
|
+
// siempre falso y la carpeta quedaría sin proteger.
|
|
32
|
+
...(await (0, middlewares_1.loadPageMiddlewares)(middlewareRouteOf(entry), ctx.moduleLoader)),
|
|
33
|
+
await (0, middlewares_1.getServerSidePropsAsMiddleware)(entry.sourceFile, ctx.moduleLoader),
|
|
34
|
+
(0, render_1.createRenderMiddleware)({ route: entry.pattern, file: entry.sourceFile, pageId: entry.pageId }, ctx.renderOptions),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* La ruta con la que se buscan los middlewares de una entrada: la que describe
|
|
39
|
+
* dónde vive el archivo, con los grupos incluidos.
|
|
40
|
+
*/
|
|
41
|
+
function middlewareRouteOf(entry) {
|
|
42
|
+
const relative = entry.sourceFile
|
|
43
|
+
.replace(/\\/g, '/')
|
|
44
|
+
.replace(/^.*app\/pages\//, '')
|
|
45
|
+
.replace(/\.[^./]+$/, '')
|
|
46
|
+
.replace(/\/?index$/, '');
|
|
47
|
+
return '/' + relative;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Construye el estado de ruteo: el manifest más la cadena de cada entrada.
|
|
51
|
+
*
|
|
52
|
+
* Se arma entero y se devuelve entero, de modo que reemplazarlo en desarrollo
|
|
53
|
+
* sea una sola asignación y ninguna petición vea una mezcla de dos versiones.
|
|
54
|
+
*/
|
|
55
|
+
async function buildRouterState(manifest, ctx) {
|
|
56
|
+
const chains = new Map();
|
|
57
|
+
for (const entry of manifest.entries) {
|
|
58
|
+
if (entry.kind === 'api')
|
|
59
|
+
continue;
|
|
60
|
+
chains.set(entry.pageId, await buildChain(entry, ctx));
|
|
61
|
+
}
|
|
62
|
+
return { manifest, chains };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* El router de páginas.
|
|
66
|
+
*
|
|
67
|
+
* Un solo despachador para todas las rutas y un solo manejador de errores, que
|
|
68
|
+
* sabe qué ruta falló porque el despachador lo dejó en la petición. El modelo
|
|
69
|
+
* anterior registraba un manejador por página.
|
|
70
|
+
*/
|
|
71
|
+
function createPageRouter(getState, ctx) {
|
|
72
|
+
const router = (0, express_1.Router)();
|
|
73
|
+
router.use((0, dispatcher_1.createDispatcher)(getState));
|
|
74
|
+
router.use((err, req, res, next) => {
|
|
75
|
+
const entry = req.ressRoute;
|
|
76
|
+
if (!entry)
|
|
77
|
+
return next(err);
|
|
78
|
+
return (0, middlewares_1.createPageErrorHandler)(entry.sourceFile, ctx.moduleLoader)(err, req, res, next);
|
|
79
|
+
});
|
|
80
|
+
return router;
|
|
81
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RouteManifestData } from './types';
|
|
2
|
+
export interface ScanOptions {
|
|
3
|
+
pagesDir?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Un conflicto en build detiene el proceso; en desarrollo avisa y sigue, para
|
|
6
|
+
* no dejar el servidor caído mientras alguien renombra un archivo.
|
|
7
|
+
*/
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Qué rutas existen en el proyecto.
|
|
12
|
+
*
|
|
13
|
+
* Lee el sistema de archivos **una vez**. El modelo anterior lo globeaba en cada
|
|
14
|
+
* arranque, también en producción, donde el árbol ya no puede cambiar.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanRoutes(options?: ScanOptions): Promise<RouteManifestData>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scanRoutes = scanRoutes;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
9
|
+
const module_extensions_1 = require("../fs/module-extensions");
|
|
10
|
+
const parse_1 = require("./parse");
|
|
11
|
+
const rank_1 = require("./rank");
|
|
12
|
+
const rank_2 = require("./rank");
|
|
13
|
+
const types_1 = require("./types");
|
|
14
|
+
/**
|
|
15
|
+
* Archivos que viven junto a las páginas pero no son rutas.
|
|
16
|
+
*
|
|
17
|
+
* `middlewares.ts` declara la cadena de su carpeta y `algo.middlewares.ts` la de
|
|
18
|
+
* su página; ninguno se navega. Los que empiezan con `_` son la convención para
|
|
19
|
+
* lo que acompaña a una página sin ser una.
|
|
20
|
+
*/
|
|
21
|
+
const NOT_A_ROUTE = /(^|\/)middlewares\.[^.]+$|\.(middlewares|test|spec|d)\.[^.]+$|(^|\/)_/;
|
|
22
|
+
/**
|
|
23
|
+
* Qué rutas existen en el proyecto.
|
|
24
|
+
*
|
|
25
|
+
* Lee el sistema de archivos **una vez**. El modelo anterior lo globeaba en cada
|
|
26
|
+
* arranque, también en producción, donde el árbol ya no puede cambiar.
|
|
27
|
+
*/
|
|
28
|
+
async function scanRoutes(options = {}) {
|
|
29
|
+
const pagesDir = options.pagesDir ?? 'app/pages';
|
|
30
|
+
// Qué extensiones cuentan lo declara la política del proyecto, no este archivo.
|
|
31
|
+
const files = await (0, fast_glob_1.default)((0, module_extensions_1.pageGlob)((0, module_extensions_1.getExtensionPolicy)(), pagesDir), { absolute: false });
|
|
32
|
+
const entries = [];
|
|
33
|
+
const byPattern = new Map();
|
|
34
|
+
const conflicts = [];
|
|
35
|
+
const manifestRoot = node_path_1.default.resolve(node_path_1.default.dirname(node_path_1.default.resolve(pagesDir)), '..');
|
|
36
|
+
for (const file of files.sort()) {
|
|
37
|
+
// fast-glob devuelve separadores nativos en Windows; el parser de rutas y
|
|
38
|
+
// los identificadores del catálogo usan `/` para que el resultado sea
|
|
39
|
+
// idéntico en todos los sistemas.
|
|
40
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
41
|
+
const sourceFile = node_path_1.default.isAbsolute(pagesDir)
|
|
42
|
+
? node_path_1.default.relative(manifestRoot, normalizedFile).replace(/\\/g, '/')
|
|
43
|
+
: normalizedFile;
|
|
44
|
+
// Se evalúa sobre el path relativo entero: una carpeta `_lib/` no es una
|
|
45
|
+
// ruta, igual que no lo es un archivo `_helper.tsx`.
|
|
46
|
+
const normalizedPagesDir = pagesDir.replace(/\\/g, '/');
|
|
47
|
+
const relative = normalizedFile.slice(normalizedFile.indexOf(normalizedPagesDir) + normalizedPagesDir.length + 1);
|
|
48
|
+
if (NOT_A_ROUTE.test('/' + relative))
|
|
49
|
+
continue;
|
|
50
|
+
const parsed = (0, parse_1.parseRoutePattern)(normalizedFile, normalizedPagesDir);
|
|
51
|
+
const entry = {
|
|
52
|
+
pageId: (0, parse_1.pageIdFromFile)(normalizedFile, normalizedPagesDir),
|
|
53
|
+
pattern: parsed.pattern,
|
|
54
|
+
segments: parsed.segments,
|
|
55
|
+
kind: (0, parse_1.classifyEntry)(normalizedFile, normalizedPagesDir),
|
|
56
|
+
sourceFile,
|
|
57
|
+
paramNames: parsed.paramNames,
|
|
58
|
+
groups: parsed.groups,
|
|
59
|
+
rank: (0, rank_1.computeRank)(parsed.segments),
|
|
60
|
+
server: { devId: '/' + sourceFile },
|
|
61
|
+
};
|
|
62
|
+
// Dos archivos que producen la misma URL son ambiguos: cuál gana dependería
|
|
63
|
+
// del orden en que el sistema de archivos los devolvió.
|
|
64
|
+
const clash = byPattern.get(`${entry.kind} ${entry.pattern}`);
|
|
65
|
+
if (clash) {
|
|
66
|
+
const message = `[ress] "${clash.sourceFile}" y "${file}" producen la misma ruta "${entry.pattern}". ` +
|
|
67
|
+
'Renombrá uno de los dos.';
|
|
68
|
+
if (options.strict)
|
|
69
|
+
throw new Error(message);
|
|
70
|
+
console.warn(`${message}\nPor ahora responde "${clash.sourceFile}".`);
|
|
71
|
+
conflicts.push({ pattern: entry.pattern, files: [clash.sourceFile, file] });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
byPattern.set(`${entry.kind} ${entry.pattern}`, entry);
|
|
75
|
+
entries.push(entry);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
version: types_1.ROUTE_MANIFEST_VERSION,
|
|
79
|
+
entries: entries.sort(rank_2.compareEntries),
|
|
80
|
+
...(conflicts.length ? { conflicts } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qué rutas existen y cuál atiende cada URL.
|
|
3
|
+
*
|
|
4
|
+
* El modelo anterior derivaba la ruta del archivo en cada arranque, globeando el
|
|
5
|
+
* sistema de archivos también en producción, y traducía el patrón a una cadena
|
|
6
|
+
* de Express que no podía expresar ni catch-all ni grupos. Acá el conjunto de
|
|
7
|
+
* rutas es un dato: se calcula una vez, se serializa en el build y se carga.
|
|
8
|
+
*/
|
|
9
|
+
export type SegmentKind = 'static' | 'dynamic' | 'catchAll' | 'optionalCatchAll';
|
|
10
|
+
export interface RouteSegment {
|
|
11
|
+
kind: SegmentKind;
|
|
12
|
+
/** Literal de URL para `static`; nombre del parámetro para el resto. */
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
/** Qué es una entrada. Determina si participa del matcheo de navegación. */
|
|
16
|
+
export type RouteEntryKind = 'page' | 'error' | 'api';
|
|
17
|
+
/** Identificador estable de una página. La misma clave que usa el catálogo de variantes. */
|
|
18
|
+
export type PageId = string;
|
|
19
|
+
export interface ServerModuleRef {
|
|
20
|
+
/** Identificador que entiende el cargador de módulos en desarrollo. */
|
|
21
|
+
devId: string;
|
|
22
|
+
/**
|
|
23
|
+
* Chunk compilado, relativo a la raíz de salida del build de servidor. Lo
|
|
24
|
+
* completa el build; en desarrollo no existe.
|
|
25
|
+
*/
|
|
26
|
+
buildChunk?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface RouteEntry {
|
|
29
|
+
pageId: PageId;
|
|
30
|
+
/** Patrón canónico con notación de archivos. Ej: `/users/[id]`. */
|
|
31
|
+
pattern: string;
|
|
32
|
+
segments: RouteSegment[];
|
|
33
|
+
kind: RouteEntryKind;
|
|
34
|
+
/** Archivo fuente, relativo a la raíz del proyecto. */
|
|
35
|
+
sourceFile: string;
|
|
36
|
+
/** Nombres de parámetro, en orden de aparición. */
|
|
37
|
+
paramNames: string[];
|
|
38
|
+
/** Grupos atravesados. Informativo: no afectan la URL. */
|
|
39
|
+
groups: string[];
|
|
40
|
+
/** Clave de precedencia precalculada. */
|
|
41
|
+
rank: number[];
|
|
42
|
+
server: ServerModuleRef;
|
|
43
|
+
/** Sólo para `error`: qué código de estado atiende. */
|
|
44
|
+
statusCode?: number;
|
|
45
|
+
/** Sólo para `api`: métodos declarados. Ausente significa todos. */
|
|
46
|
+
methods?: string[];
|
|
47
|
+
}
|
|
48
|
+
export interface RouteMatch {
|
|
49
|
+
entry: RouteEntry;
|
|
50
|
+
params: Record<string, string | string[]>;
|
|
51
|
+
}
|
|
52
|
+
export interface MatchOptions {
|
|
53
|
+
/** Qué clases de entrada participan. Por defecto sólo `page`. */
|
|
54
|
+
kinds?: readonly RouteEntryKind[];
|
|
55
|
+
}
|
|
56
|
+
/** Dos archivos que compiten por el mismo patrón, detectados al escanear. */
|
|
57
|
+
export interface RouteConflict {
|
|
58
|
+
pattern: string;
|
|
59
|
+
files: string[];
|
|
60
|
+
}
|
|
61
|
+
/** Lo que se serializa al disco. */
|
|
62
|
+
export interface RouteManifestData {
|
|
63
|
+
/** Sube cuando el formato cambia de forma incompatible. */
|
|
64
|
+
version: number;
|
|
65
|
+
entries: RouteEntry[];
|
|
66
|
+
/**
|
|
67
|
+
* Conflictos detectados en modo no estricto (desarrollo).
|
|
68
|
+
*
|
|
69
|
+
* En modo estricto (`build`) `scanRoutes` lanza en vez de completar esta
|
|
70
|
+
* lista: un build no deja artefactos parciales publicables. F-033 (`ress
|
|
71
|
+
* info`) es quien la consume para diagnosticar sin abortar.
|
|
72
|
+
*/
|
|
73
|
+
conflicts?: RouteConflict[];
|
|
74
|
+
}
|
|
75
|
+
export interface RouteManifest {
|
|
76
|
+
readonly entries: readonly RouteEntry[];
|
|
77
|
+
/** Qué entrada atiende esta URL. `undefined` si ninguna. */
|
|
78
|
+
match(pathname: string, options?: MatchOptions): RouteMatch | undefined;
|
|
79
|
+
/** Una entrada por su identificador. */
|
|
80
|
+
get(pageId: PageId): RouteEntry | undefined;
|
|
81
|
+
/** Las entradas de una clase, en orden de precedencia. */
|
|
82
|
+
byKind(kind: RouteEntryKind): readonly RouteEntry[];
|
|
83
|
+
}
|
|
84
|
+
/** Sube cuando el formato del manifest cambia de forma incompatible. */
|
|
85
|
+
export declare const ROUTE_MANIFEST_VERSION = 1;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Qué rutas existen y cuál atiende cada URL.
|
|
4
|
+
*
|
|
5
|
+
* El modelo anterior derivaba la ruta del archivo en cada arranque, globeando el
|
|
6
|
+
* sistema de archivos también en producción, y traducía el patrón a una cadena
|
|
7
|
+
* de Express que no podía expresar ni catch-all ni grupos. Acá el conjunto de
|
|
8
|
+
* rutas es un dato: se calcula una vez, se serializa en el build y se carga.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ROUTE_MANIFEST_VERSION = void 0;
|
|
12
|
+
/** Sube cuando el formato del manifest cambia de forma incompatible. */
|
|
13
|
+
exports.ROUTE_MANIFEST_VERSION = 1;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime de desarrollo.
|
|
3
|
+
*
|
|
4
|
+
* Crea el servidor de Vite, resuelve el entorno de servidor y construye el cargador de
|
|
5
|
+
* módulos una sola vez, que es el que usa el resto del framework.
|
|
6
|
+
*/
|
|
7
|
+
import { type FetchableEnvironment, type ModuleLoader } from './module-loader';
|
|
8
|
+
import type { AssetIndex, VariantCatalog } from '../variants/types';
|
|
9
|
+
import type { RouteManifest } from '../routes/types';
|
|
10
|
+
export interface RessRuntime {
|
|
11
|
+
/** Instancia de Vite. `null` en producción. */
|
|
12
|
+
vite: any | null;
|
|
13
|
+
/** Carga módulos de usuario. Único en dev y prod. */
|
|
14
|
+
moduleLoader: ModuleLoader;
|
|
15
|
+
templateHtml: string;
|
|
16
|
+
isProduction: boolean;
|
|
17
|
+
/** Qué variantes tiene cada página. Se arma una vez al arrancar. */
|
|
18
|
+
catalog: VariantCatalog;
|
|
19
|
+
/** Qué rutas existen y cuál atiende cada URL. */
|
|
20
|
+
routes: RouteManifest;
|
|
21
|
+
/** Artefactos por página y variante. */
|
|
22
|
+
assets: AssetIndex;
|
|
23
|
+
/** Archivos que emitió el build de cliente. Vacío en desarrollo. */
|
|
24
|
+
buildOutputs: ReadonlySet<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Registra un aviso de cambio en el código de usuario. Devuelve la función
|
|
27
|
+
* para dejar de escuchar. En producción no se invoca nunca.
|
|
28
|
+
*/
|
|
29
|
+
onSourceChange(cb: (file: string) => void): () => void;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Elige el entorno desde el que se cargarán los módulos de usuario.
|
|
34
|
+
*
|
|
35
|
+
* Se prefiere `ssr` por nombre; si un proyecto declaró otros entornos, se cae al
|
|
36
|
+
* primero disponible en vez de fallar. Nunca se asume que el entorno sea del
|
|
37
|
+
* tipo ejecutable: el cargador se construye sobre `fetchModule()`, presente en
|
|
38
|
+
* todos los tipos.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveSsrEnvironment(server: any): FetchableEnvironment;
|
|
41
|
+
export interface DevRuntimeOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Plugins de Vite a aplicar, además de los que el proyecto declare en su
|
|
44
|
+
* propio `vite.config.*` si lo tiene.
|
|
45
|
+
*
|
|
46
|
+
* F-033 (la CLI) los necesita: un proyecto sin `vite.config.ts` no tiene
|
|
47
|
+
* quién le pase `ressPlugin()` a Vite si `createDevRuntime` no acepta
|
|
48
|
+
* plugins inline. Antes de esta opción, `createServer` dependía por completo
|
|
49
|
+
* de que Vite encontrara un archivo de configuración en el directorio de
|
|
50
|
+
* trabajo — que es justamente lo que la CLI existe para volver innecesario.
|
|
51
|
+
*/
|
|
52
|
+
plugins?: unknown[];
|
|
53
|
+
}
|
|
54
|
+
export declare function createDevRuntime(base?: string,
|
|
55
|
+
/**
|
|
56
|
+
* Prefijo con el que se emiten las URL de artefactos.
|
|
57
|
+
*
|
|
58
|
+
* Va aparte del `base` de Vite porque el prefijo de ruta lo administra el
|
|
59
|
+
* framework: el pipeline de petición lo quita antes de que Vite vea la URL, así
|
|
60
|
+
* que Vite sigue viviendo en `/` y sólo lo que se escribe en el HTML lo lleva.
|
|
61
|
+
*/
|
|
62
|
+
assetPrefix?: string, options?: DevRuntimeOptions): Promise<RessRuntime>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime de desarrollo.
|
|
4
|
+
*
|
|
5
|
+
* Crea el servidor de Vite, resuelve el entorno de servidor y construye el cargador de
|
|
6
|
+
* módulos una sola vez, que es el que usa el resto del framework.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resolveSsrEnvironment = resolveSsrEnvironment;
|
|
10
|
+
exports.createDevRuntime = createDevRuntime;
|
|
11
|
+
const module_loader_1 = require("./module-loader");
|
|
12
|
+
const assets_1 = require("../variants/assets");
|
|
13
|
+
const manifest_1 = require("../routes/manifest");
|
|
14
|
+
const scan_1 = require("../routes/scan");
|
|
15
|
+
const index_1 = require("../plugin/index");
|
|
16
|
+
const template_1 = require("./template");
|
|
17
|
+
/**
|
|
18
|
+
* Elige el entorno desde el que se cargarán los módulos de usuario.
|
|
19
|
+
*
|
|
20
|
+
* Se prefiere `ssr` por nombre; si un proyecto declaró otros entornos, se cae al
|
|
21
|
+
* primero disponible en vez de fallar. Nunca se asume que el entorno sea del
|
|
22
|
+
* tipo ejecutable: el cargador se construye sobre `fetchModule()`, presente en
|
|
23
|
+
* todos los tipos.
|
|
24
|
+
*/
|
|
25
|
+
function resolveSsrEnvironment(server) {
|
|
26
|
+
const envs = server?.environments;
|
|
27
|
+
if (!envs) {
|
|
28
|
+
throw new Error('[ress] El servidor de Vite no expone environments. ' +
|
|
29
|
+
'ress.js requiere Vite 8 o superior.');
|
|
30
|
+
}
|
|
31
|
+
const env = envs.ssr ?? Object.values(envs)[0];
|
|
32
|
+
if (!env || typeof env.fetchModule !== 'function') {
|
|
33
|
+
throw new Error('[ress] No se encontró un entorno de servidor utilizable. ' +
|
|
34
|
+
`Entornos disponibles: ${Object.keys(envs).join(', ') || '(ninguno)'}`);
|
|
35
|
+
}
|
|
36
|
+
return env;
|
|
37
|
+
}
|
|
38
|
+
async function createDevRuntime(base = '/',
|
|
39
|
+
/**
|
|
40
|
+
* Prefijo con el que se emiten las URL de artefactos.
|
|
41
|
+
*
|
|
42
|
+
* Va aparte del `base` de Vite porque el prefijo de ruta lo administra el
|
|
43
|
+
* framework: el pipeline de petición lo quita antes de que Vite vea la URL, así
|
|
44
|
+
* que Vite sigue viviendo en `/` y sólo lo que se escribe en el HTML lo lleva.
|
|
45
|
+
*/
|
|
46
|
+
assetPrefix = base, options = {}) {
|
|
47
|
+
const { createServer } = await import('vite');
|
|
48
|
+
const vite = await createServer({
|
|
49
|
+
server: { middlewareMode: true },
|
|
50
|
+
appType: 'custom',
|
|
51
|
+
base,
|
|
52
|
+
plugins: options.plugins,
|
|
53
|
+
});
|
|
54
|
+
const env = resolveSsrEnvironment(vite);
|
|
55
|
+
const moduleLoader = await (0, module_loader_1.createDevModuleLoader)(env);
|
|
56
|
+
// El runner mantiene su propia caché de módulos evaluados y corre sin canal de
|
|
57
|
+
// HMR, así que nada la descarta por su cuenta. Conectar el watcher es lo que
|
|
58
|
+
// hace que editar una página o un middleware se refleje en la siguiente
|
|
59
|
+
// petición en vez de exigir reiniciar el servidor.
|
|
60
|
+
const listeners = new Set();
|
|
61
|
+
const onFileChange = (file) => {
|
|
62
|
+
moduleLoader.invalidate(file);
|
|
63
|
+
for (const cb of listeners)
|
|
64
|
+
cb(file);
|
|
65
|
+
};
|
|
66
|
+
vite.watcher.on('change', onFileChange);
|
|
67
|
+
vite.watcher.on('unlink', onFileChange);
|
|
68
|
+
const templateHtml = await (0, template_1.readTemplateHtml)();
|
|
69
|
+
// El plugin lo arma al resolver la configuración; si por algún motivo no
|
|
70
|
+
// corrió, se arma acá para no dejar el runtime sin catálogo.
|
|
71
|
+
const catalog = (0, index_1.getVariantCatalog)() ?? (await (0, index_1.refreshVariantCatalog)());
|
|
72
|
+
const routes = (0, manifest_1.createRouteManifest)(await (0, scan_1.scanRoutes)());
|
|
73
|
+
return {
|
|
74
|
+
vite,
|
|
75
|
+
moduleLoader,
|
|
76
|
+
templateHtml,
|
|
77
|
+
isProduction: false,
|
|
78
|
+
catalog,
|
|
79
|
+
routes,
|
|
80
|
+
assets: (0, assets_1.createDevAssetIndex)(catalog, assetPrefix),
|
|
81
|
+
// En desarrollo los sirve Vite, no el middleware de estáticos.
|
|
82
|
+
buildOutputs: new Set(),
|
|
83
|
+
onSourceChange: (cb) => {
|
|
84
|
+
listeners.add(cb);
|
|
85
|
+
return () => listeners.delete(cb);
|
|
86
|
+
},
|
|
87
|
+
close: async () => {
|
|
88
|
+
vite.watcher.off('change', onFileChange);
|
|
89
|
+
vite.watcher.off('unlink', onFileChange);
|
|
90
|
+
await moduleLoader.close();
|
|
91
|
+
await vite.close();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|