@ressjs/vite-router 0.5.2 → 0.6.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +69 -0
  2. package/README.md +9 -515
  3. package/dist/client/entry.d.mts +2 -0
  4. package/dist/client/entry.mjs +1 -0
  5. package/dist/client/index.d.ts +2 -0
  6. package/dist/client/index.js +8 -0
  7. package/dist/config/base-path.d.ts +44 -0
  8. package/dist/config/base-path.js +100 -0
  9. package/dist/config/codegen.d.ts +29 -0
  10. package/dist/config/codegen.js +131 -0
  11. package/dist/config/define.d.ts +8 -0
  12. package/dist/config/define.js +12 -0
  13. package/dist/config/env.d.ts +19 -0
  14. package/dist/config/env.js +40 -0
  15. package/dist/config/index.d.ts +18 -0
  16. package/dist/config/index.js +46 -0
  17. package/dist/config/load.d.ts +28 -0
  18. package/dist/config/load.js +82 -0
  19. package/dist/config/middleware.d.ts +18 -0
  20. package/dist/config/middleware.js +79 -0
  21. package/dist/config/rules.d.ts +59 -0
  22. package/dist/config/rules.js +162 -0
  23. package/dist/config/types.d.ts +189 -0
  24. package/dist/config/types.js +2 -0
  25. package/dist/config/validate.d.ts +19 -0
  26. package/dist/config/validate.js +143 -0
  27. package/dist/config/watch.d.ts +53 -0
  28. package/dist/config/watch.js +163 -0
  29. package/dist/fs/module-extensions.d.ts +105 -0
  30. package/dist/fs/module-extensions.js +213 -0
  31. package/dist/head/resolve.d.ts +5 -0
  32. package/dist/head/resolve.js +166 -0
  33. package/dist/head/types.d.ts +44 -0
  34. package/dist/head/types.js +2 -0
  35. package/dist/helpers/html-generator.d.ts +26 -6
  36. package/dist/helpers/html-generator.js +96 -138
  37. package/dist/helpers/middlewares.d.ts +32 -21
  38. package/dist/helpers/middlewares.js +181 -159
  39. package/dist/helpers/page-config-merge.d.ts +11 -0
  40. package/dist/helpers/page-config-merge.js +84 -0
  41. package/dist/helpers/request-handler.d.ts +15 -2
  42. package/dist/helpers/request-handler.js +164 -27
  43. package/dist/index.d.ts +35 -3
  44. package/dist/index.js +77 -8
  45. package/dist/isr/capture.d.ts +79 -0
  46. package/dist/isr/capture.js +222 -0
  47. package/dist/isr/handler.d.ts +49 -0
  48. package/dist/isr/handler.js +207 -0
  49. package/dist/isr/key.d.ts +26 -0
  50. package/dist/isr/key.js +59 -0
  51. package/dist/isr/page-config.d.ts +17 -0
  52. package/dist/isr/page-config.js +71 -0
  53. package/dist/isr/preview.d.ts +5 -0
  54. package/dist/isr/preview.js +38 -0
  55. package/dist/isr/public-request.d.ts +50 -0
  56. package/dist/isr/public-request.js +108 -0
  57. package/dist/isr/response.d.ts +41 -0
  58. package/dist/isr/response.js +128 -0
  59. package/dist/isr/store.d.ts +42 -0
  60. package/dist/isr/store.js +108 -0
  61. package/dist/isr/types.d.ts +62 -0
  62. package/dist/isr/types.js +2 -0
  63. package/dist/pages.d.ts +10 -9
  64. package/dist/pages.js +42 -261
  65. package/dist/platform.d.ts +11 -14
  66. package/dist/platform.js +18 -102
  67. package/dist/plugin/environments.d.ts +68 -0
  68. package/dist/plugin/environments.js +74 -0
  69. package/dist/plugin/index.d.ts +59 -0
  70. package/dist/plugin/index.js +195 -0
  71. package/dist/plugin/route-manifest.d.ts +19 -0
  72. package/dist/plugin/route-manifest.js +55 -0
  73. package/dist/plugin/virtual-entries.d.ts +26 -0
  74. package/dist/plugin/virtual-entries.js +80 -0
  75. package/dist/render.d.ts +39 -2
  76. package/dist/render.js +75 -77
  77. package/dist/router.d.ts +32 -13
  78. package/dist/router.js +180 -146
  79. package/dist/routes/dispatcher.d.ts +58 -0
  80. package/dist/routes/dispatcher.js +70 -0
  81. package/dist/routes/manifest.d.ts +17 -0
  82. package/dist/routes/manifest.js +62 -0
  83. package/dist/routes/match.d.ts +20 -0
  84. package/dist/routes/match.js +75 -0
  85. package/dist/routes/module.d.ts +14 -0
  86. package/dist/routes/module.js +30 -0
  87. package/dist/routes/parse.d.ts +31 -0
  88. package/dist/routes/parse.js +114 -0
  89. package/dist/routes/rank.d.ts +12 -0
  90. package/dist/routes/rank.js +49 -0
  91. package/dist/routes/router.d.ts +54 -0
  92. package/dist/routes/router.js +153 -0
  93. package/dist/routes/scan.d.ts +26 -0
  94. package/dist/routes/scan.js +98 -0
  95. package/dist/routes/types.d.ts +114 -0
  96. package/dist/routes/types.js +17 -0
  97. package/dist/runtime/dev-server.d.ts +64 -0
  98. package/dist/runtime/dev-server.js +94 -0
  99. package/dist/runtime/dev-styles.d.ts +13 -0
  100. package/dist/runtime/dev-styles.js +50 -0
  101. package/dist/runtime/module-loader.d.ts +55 -0
  102. package/dist/runtime/module-loader.js +122 -0
  103. package/dist/runtime/prod-server.d.ts +55 -0
  104. package/dist/runtime/prod-server.js +188 -0
  105. package/dist/runtime/template.d.ts +10 -0
  106. package/dist/runtime/template.js +33 -0
  107. package/dist/security/client-props.d.ts +25 -0
  108. package/dist/security/client-props.js +51 -0
  109. package/dist/security/config.d.ts +121 -0
  110. package/dist/security/config.js +77 -0
  111. package/dist/security/csp.d.ts +53 -0
  112. package/dist/security/csp.js +118 -0
  113. package/dist/security/dev-hardening.d.ts +46 -0
  114. package/dist/security/dev-hardening.js +65 -0
  115. package/dist/security/errors.d.ts +37 -0
  116. package/dist/security/errors.js +84 -0
  117. package/dist/security/escape.d.ts +40 -0
  118. package/dist/security/escape.js +90 -0
  119. package/dist/security/head-tags.d.ts +32 -0
  120. package/dist/security/head-tags.js +156 -0
  121. package/dist/security/headers.d.ts +76 -0
  122. package/dist/security/headers.js +278 -0
  123. package/dist/security/index.d.ts +24 -0
  124. package/dist/security/index.js +49 -0
  125. package/dist/security/serialize.d.ts +48 -0
  126. package/dist/security/serialize.js +146 -0
  127. package/dist/variants/assets.d.ts +33 -0
  128. package/dist/variants/assets.js +144 -0
  129. package/dist/variants/catalog.d.ts +33 -0
  130. package/dist/variants/catalog.js +79 -0
  131. package/dist/variants/platform-tokens.d.ts +9 -0
  132. package/dist/variants/platform-tokens.js +15 -0
  133. package/dist/variants/resolve.d.ts +40 -0
  134. package/dist/variants/resolve.js +90 -0
  135. package/dist/variants/suffix.d.ts +8 -0
  136. package/dist/variants/suffix.js +12 -0
  137. package/dist/variants/types.d.ts +72 -0
  138. package/dist/variants/types.js +2 -0
  139. package/package.json +23 -11
  140. package/dist/helpers/vite-config.d.ts +0 -10
  141. package/dist/helpers/vite-config.js +0 -90
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDispatcher = createDispatcher;
4
+ exports.runMiddlewareChain = runMiddlewareChain;
5
+ /**
6
+ * Un único punto de entrada para todas las rutas.
7
+ *
8
+ * El modelo anterior registraba una ruta de Express por página, lo que obligaba
9
+ * a traducir cada patrón a la sintaxis de Express —que no puede expresar un
10
+ * catch-all opcional ni un grupo— y dejaba la precedencia en manos del orden de
11
+ * registro. Acá el manifest decide, y Express sólo ve un middleware.
12
+ *
13
+ * Las cadenas se construyen una vez, no por petición. `chains` se reemplaza
14
+ * entero cuando el manifest cambia en desarrollo, de modo que una petición nunca
15
+ * ve una mezcla de dos versiones.
16
+ */
17
+ function createDispatcher(getState) {
18
+ return async (req, res, next) => {
19
+ const { manifest, chains } = getState();
20
+ const match = manifest.match(req.path ?? req.url ?? '/');
21
+ // Sin match no es un 404 todavía: puede haber middlewares detrás que sepan
22
+ // atenderla, y decidirlo acá les quitaría la oportunidad.
23
+ if (!match)
24
+ return next();
25
+ const chain = chains.get(match.entry.pageId);
26
+ if (!chain)
27
+ return next();
28
+ req.ressParams = match.params;
29
+ req.params = match.params;
30
+ req.ressRoute = match.entry;
31
+ runMiddlewareChain(chain, req, res, next);
32
+ };
33
+ }
34
+ /**
35
+ * Ejecuta la cadena respetando el contrato de Express.
36
+ *
37
+ * Cada middleware decide si sigue: `next()` avanza, `next(error)` corta y
38
+ * propaga. Un throw síncrono se convierte en `next(error)` para que no quede una
39
+ * petición colgada.
40
+ */
41
+ function runMiddlewareChain(chain, req, res, done) {
42
+ let index = 0;
43
+ const step = (error) => {
44
+ if (error)
45
+ return done(error);
46
+ const handler = chain[index++];
47
+ if (!handler)
48
+ return done();
49
+ // Un middleware asíncrono puede llamar a `next()` y rechazar después. Sin
50
+ // esta guarda, el rechazo volvería a entrar a la cadena sobre una respuesta
51
+ // que ya se envió, y el fallo sería un ERR_HTTP_HEADERS_SENT en vez de la
52
+ // página de error.
53
+ let advanced = false;
54
+ const once = (err) => {
55
+ if (advanced)
56
+ return;
57
+ advanced = true;
58
+ step(err);
59
+ };
60
+ try {
61
+ const result = handler(req, res, once);
62
+ if (result instanceof Promise)
63
+ result.catch(once);
64
+ }
65
+ catch (err) {
66
+ once(err);
67
+ }
68
+ };
69
+ step();
70
+ }
@@ -0,0 +1,17 @@
1
+ import { type RouteManifest, type RouteManifestData } from './types';
2
+ /**
3
+ * El manifest ya indexado.
4
+ *
5
+ * Las rutas estáticas —la mayoría de un proyecto— se resuelven con una consulta
6
+ * a un mapa. Sólo las que tienen parámetros recorren la lista, y ya viene
7
+ * ordenada por precedencia, así que la primera que matchea es la que gana.
8
+ */
9
+ export declare function createRouteManifest(data: RouteManifestData): RouteManifest;
10
+ export declare function serializeRouteManifest(data: RouteManifestData): string;
11
+ /**
12
+ * Lee un manifest emitido por el build.
13
+ *
14
+ * La versión se comprueba a propósito: un manifest de un formato anterior
15
+ * produciría fallos de ruteo difíciles de atribuir, y el mensaje dice qué hacer.
16
+ */
17
+ export declare function loadRouteManifest(raw: string): RouteManifest;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRouteManifest = createRouteManifest;
4
+ exports.serializeRouteManifest = serializeRouteManifest;
5
+ exports.loadRouteManifest = loadRouteManifest;
6
+ const match_1 = require("./match");
7
+ const types_1 = require("./types");
8
+ /**
9
+ * El manifest ya indexado.
10
+ *
11
+ * Las rutas estáticas —la mayoría de un proyecto— se resuelven con una consulta
12
+ * a un mapa. Sólo las que tienen parámetros recorren la lista, y ya viene
13
+ * ordenada por precedencia, así que la primera que matchea es la que gana.
14
+ */
15
+ function createRouteManifest(data) {
16
+ const entries = data.entries;
17
+ const byPageId = new Map(entries.map((e) => [e.pageId, e]));
18
+ const staticIndex = new Map();
19
+ for (const entry of entries) {
20
+ if (entry.segments.every((s) => s.kind === 'static')) {
21
+ staticIndex.set(`${entry.kind} ${entry.pattern}`, entry);
22
+ }
23
+ }
24
+ return {
25
+ entries,
26
+ middlewares: data.middlewares,
27
+ match(pathname, options = {}) {
28
+ const kinds = options.kinds ?? ['page'];
29
+ const clean = '/' + pathname.split('?')[0].split('#')[0].split('/').filter(Boolean).join('/');
30
+ for (const kind of kinds) {
31
+ const exact = staticIndex.get(`${kind} ${clean}`);
32
+ if (exact)
33
+ return { entry: exact, params: {} };
34
+ }
35
+ return (0, match_1.matchEntries)(entries, pathname, options);
36
+ },
37
+ get: (pageId) => byPageId.get(pageId),
38
+ byKind: (kind) => entries.filter((e) => e.kind === kind),
39
+ };
40
+ }
41
+ function serializeRouteManifest(data) {
42
+ return JSON.stringify(data, null, 2);
43
+ }
44
+ /**
45
+ * Lee un manifest emitido por el build.
46
+ *
47
+ * La versión se comprueba a propósito: un manifest de un formato anterior
48
+ * produciría fallos de ruteo difíciles de atribuir, y el mensaje dice qué hacer.
49
+ */
50
+ function loadRouteManifest(raw) {
51
+ const data = JSON.parse(raw);
52
+ if (data.version !== types_1.ROUTE_MANIFEST_VERSION) {
53
+ throw new Error(`[ress] El manifest de rutas es de la versión ${data.version} y este ress.js espera la ` +
54
+ `${types_1.ROUTE_MANIFEST_VERSION}. Volvé a construir el proyecto.`);
55
+ }
56
+ // Un manifest de build sin la lista de middlewares arrancaría sin ellos. Si
57
+ // falta, el build está incompleto: mejor no arrancar.
58
+ if (!Array.isArray(data.middlewares)) {
59
+ throw new Error('[ress] El manifest de rutas no registra los middlewares del build. Volvé a construir el proyecto.');
60
+ }
61
+ return createRouteManifest(data);
62
+ }
@@ -0,0 +1,20 @@
1
+ import type { MatchOptions, RouteEntry, RouteMatch, RouteSegment } from './types';
2
+ /**
3
+ * Parte una URL en segmentos.
4
+ *
5
+ * Se descartan la consulta y el fragmento, y la barra final no distingue: `/about`
6
+ * y `/about/` son la misma ruta.
7
+ */
8
+ export declare function splitPath(pathname: string): string[];
9
+ /**
10
+ * ¿Esta ruta atiende esta URL?
11
+ *
12
+ * Sin expresiones regulares: se recorre segmento a segmento. Una regex construida
13
+ * a partir de un patrón es difícil de auditar y fácil de romper con un carácter
14
+ * que había que escapar — y el modelo anterior tenía justamente ese defecto:
15
+ * `/page1` matcheaba `/page1/cualquier/cosa` porque su regex no exigía consumir
16
+ * la URL entera.
17
+ */
18
+ export declare function matchSegments(segments: readonly RouteSegment[], parts: readonly string[]): Record<string, string | string[]> | undefined;
19
+ /** La primera entrada que atiende esta URL, recorriendo en orden de precedencia. */
20
+ export declare function matchEntries(entries: readonly RouteEntry[], pathname: string, options?: MatchOptions): RouteMatch | undefined;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitPath = splitPath;
4
+ exports.matchSegments = matchSegments;
5
+ exports.matchEntries = matchEntries;
6
+ /**
7
+ * Parte una URL en segmentos.
8
+ *
9
+ * Se descartan la consulta y el fragmento, y la barra final no distingue: `/about`
10
+ * y `/about/` son la misma ruta.
11
+ */
12
+ function splitPath(pathname) {
13
+ const clean = pathname.split('?')[0].split('#')[0];
14
+ return clean.split('/').filter(Boolean);
15
+ }
16
+ /**
17
+ * ¿Esta ruta atiende esta URL?
18
+ *
19
+ * Sin expresiones regulares: se recorre segmento a segmento. Una regex construida
20
+ * a partir de un patrón es difícil de auditar y fácil de romper con un carácter
21
+ * que había que escapar — y el modelo anterior tenía justamente ese defecto:
22
+ * `/page1` matcheaba `/page1/cualquier/cosa` porque su regex no exigía consumir
23
+ * la URL entera.
24
+ */
25
+ function matchSegments(segments, parts) {
26
+ const params = {};
27
+ let index = 0;
28
+ for (const segment of segments) {
29
+ if (segment.kind === 'catchAll' || segment.kind === 'optionalCatchAll') {
30
+ const rest = parts.slice(index).map(decode);
31
+ // Un catch-all obligatorio necesita al menos un segmento; el opcional
32
+ // acepta ninguno y entrega una lista vacía.
33
+ if (segment.kind === 'catchAll' && rest.length === 0)
34
+ return undefined;
35
+ params[segment.value] = rest;
36
+ return params;
37
+ }
38
+ const part = parts[index];
39
+ if (part === undefined)
40
+ return undefined;
41
+ if (segment.kind === 'static') {
42
+ if (part !== segment.value)
43
+ return undefined;
44
+ }
45
+ else {
46
+ params[segment.value] = decode(part);
47
+ }
48
+ index++;
49
+ }
50
+ // La URL tiene que quedar consumida entera: lo que sobra no lo atiende esta ruta.
51
+ return index === parts.length ? params : undefined;
52
+ }
53
+ function decode(value) {
54
+ try {
55
+ return decodeURIComponent(value);
56
+ }
57
+ catch {
58
+ // Una secuencia de escape inválida no es motivo para fallar la petición: el
59
+ // valor crudo es lo que el cliente pidió.
60
+ return value;
61
+ }
62
+ }
63
+ /** La primera entrada que atiende esta URL, recorriendo en orden de precedencia. */
64
+ function matchEntries(entries, pathname, options = {}) {
65
+ const kinds = options.kinds ?? ['page'];
66
+ const parts = splitPath(pathname);
67
+ for (const entry of entries) {
68
+ if (!kinds.includes(entry.kind))
69
+ continue;
70
+ const params = matchSegments(entry.segments, parts);
71
+ if (params)
72
+ return { entry, params };
73
+ }
74
+ return undefined;
75
+ }
@@ -0,0 +1,14 @@
1
+ import type { RouteEntry } from './types';
2
+ export interface ResolveServerModuleContext {
3
+ root: string;
4
+ isProduction: boolean;
5
+ /** Directorio del build SSR, relativo a `root` o absoluto. */
6
+ serverOutDir: string;
7
+ }
8
+ /**
9
+ * Resuelve el módulo de una entrada desde la identidad que registró el build.
10
+ *
11
+ * En producción no se vuelve a derivar un nombre desde `sourceFile`: esa regla
12
+ * se rompe apenas el bundler cambia un nombre, agrega un hash o mueve un chunk.
13
+ */
14
+ export declare function resolveServerModule(entry: Pick<RouteEntry, 'pageId' | 'server'>, context: ResolveServerModuleContext): string;
@@ -0,0 +1,30 @@
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.resolveServerModule = resolveServerModule;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ /**
9
+ * Resuelve el módulo de una entrada desde la identidad que registró el build.
10
+ *
11
+ * En producción no se vuelve a derivar un nombre desde `sourceFile`: esa regla
12
+ * se rompe apenas el bundler cambia un nombre, agrega un hash o mueve un chunk.
13
+ */
14
+ function resolveServerModule(entry, context) {
15
+ if (!context.isProduction)
16
+ return entry.server.devId;
17
+ const chunk = entry.server.buildChunk;
18
+ if (!chunk) {
19
+ throw new Error(`[ress] La entrada "${entry.pageId}" no tiene módulo de servidor compilado. ` +
20
+ 'Volvé a construir el proyecto.');
21
+ }
22
+ const outDir = node_path_1.default.resolve(context.root, context.serverOutDir);
23
+ const target = node_path_1.default.resolve(outDir, chunk);
24
+ const relative = node_path_1.default.relative(outDir, target);
25
+ if (relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative)) {
26
+ throw new Error(`[ress] El chunk de servidor de "${entry.pageId}" queda fuera del directorio de build: ` +
27
+ `"${chunk}".`);
28
+ }
29
+ return target;
30
+ }
@@ -0,0 +1,31 @@
1
+ import type { RouteEntryKind, RouteSegment } from './types';
2
+ /**
3
+ * Convierte la ubicación de un archivo en un patrón de ruta.
4
+ *
5
+ * Notación:
6
+ *
7
+ * | En el archivo | En la URL |
8
+ * |---|---|
9
+ * | `users/perfil.tsx` | `/users/perfil` |
10
+ * | `users/index.tsx` | `/users` |
11
+ * | `users/[id].tsx` | `/users/:id` |
12
+ * | `docs/[...slug].tsx` | `/docs/` y todo lo que siga |
13
+ * | `shop/[[...filtros]].tsx` | `/shop` y todo lo que siga |
14
+ * | `(marketing)/about.tsx` | `/about` — el grupo no aporta segmento |
15
+ */
16
+ export interface ParsedRoute {
17
+ pattern: string;
18
+ segments: RouteSegment[];
19
+ paramNames: string[];
20
+ groups: string[];
21
+ }
22
+ export declare function parseRoutePattern(sourceFile: string, pagesDir?: string): ParsedRoute;
23
+ /**
24
+ * Identificador estable de una página.
25
+ *
26
+ * Es la misma clave que usa el catálogo de variantes de F-016, para que las dos
27
+ * estructuras hablen del mismo objeto sin traducir nada entre ellas.
28
+ */
29
+ export declare function pageIdFromFile(sourceFile: string, pagesDir?: string): string;
30
+ /** Qué clase de entrada es, según dónde vive el archivo. */
31
+ export declare function classifyEntry(sourceFile: string, pagesDir?: string): RouteEntryKind;
@@ -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,54 @@
1
+ import { Router, type RequestHandler } from 'express';
2
+ import { type MiddlewareInfo, type PageModule } from '../helpers/middlewares';
3
+ import { type IsrRuntime } from '../isr/handler';
4
+ import type { ModuleLoader } from '../runtime/module-loader';
5
+ import type { RouteEntry, RouteManifest } from './types';
6
+ export interface ChainContext {
7
+ moduleLoader?: ModuleLoader;
8
+ /**
9
+ * Middlewares que registró el build. Si están, la cadena se arma con ellos y
10
+ * no se busca en el sistema de archivos; `buildRouterState` los toma del
11
+ * manifest.
12
+ */
13
+ middlewares?: readonly MiddlewareInfo[];
14
+ renderOptions: {
15
+ enablePlatformDetection: boolean;
16
+ renderFunction?: (options: {
17
+ url: string;
18
+ platform: string;
19
+ req: any;
20
+ }) => Promise<string>;
21
+ viteConfig: unknown;
22
+ security: unknown;
23
+ };
24
+ /** Caché incremental. Sin ella, una página `isr` se comporta como `ssr`. */
25
+ isr?: IsrRuntime;
26
+ }
27
+ /**
28
+ * La cadena de middlewares de una entrada.
29
+ *
30
+ * Es el cuerpo que estaba duplicado entre los dos constructores de router del
31
+ * modelo anterior: lo que hace un middleware de carpeta, `getServerSideProps` y
32
+ * el render, en ese orden. Tenerlo una vez es lo que garantiza que los dos
33
+ * caminos se comporten igual.
34
+ */
35
+ export declare function buildChain(entry: RouteEntry, ctx: ChainContext, preloaded?: PageModule | null): Promise<RequestHandler[]>;
36
+ export interface RessRouterState {
37
+ manifest: RouteManifest;
38
+ chains: Map<string, RequestHandler[]>;
39
+ }
40
+ /**
41
+ * Construye el estado de ruteo: el manifest más la cadena de cada entrada.
42
+ *
43
+ * Se arma entero y se devuelve entero, de modo que reemplazarlo en desarrollo
44
+ * sea una sola asignación y ninguna petición vea una mezcla de dos versiones.
45
+ */
46
+ export declare function buildRouterState(manifest: RouteManifest, ctx: ChainContext): Promise<RessRouterState>;
47
+ /**
48
+ * El router de páginas.
49
+ *
50
+ * Un solo despachador para todas las rutas y un solo manejador de errores, que
51
+ * sabe qué ruta falló porque el despachador lo dejó en la petición. El modelo
52
+ * anterior registraba un manejador por página.
53
+ */
54
+ export declare function createPageRouter(getState: () => RessRouterState, ctx: ChainContext): Router;