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

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 (50) hide show
  1. package/dist/config/codegen.d.ts +10 -1
  2. package/dist/config/codegen.js +15 -9
  3. package/dist/config/env.d.ts +2 -0
  4. package/dist/config/env.js +6 -3
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +2 -1
  7. package/dist/config/load.d.ts +0 -1
  8. package/dist/config/load.js +2 -1
  9. package/dist/config/types.d.ts +8 -5
  10. package/dist/config/validate.js +67 -0
  11. package/dist/config/watch.d.ts +22 -0
  12. package/dist/config/watch.js +57 -0
  13. package/dist/fs/canonical-path.d.ts +12 -0
  14. package/dist/fs/canonical-path.js +28 -0
  15. package/dist/fs/shared-modules.d.ts +22 -0
  16. package/dist/fs/shared-modules.js +44 -0
  17. package/dist/helpers/request-handler.js +0 -8
  18. package/dist/index.d.ts +6 -5
  19. package/dist/index.js +9 -2
  20. package/dist/isr/handler.d.ts +2 -0
  21. package/dist/isr/handler.js +1 -1
  22. package/dist/isr/response.d.ts +3 -1
  23. package/dist/isr/response.js +4 -2
  24. package/dist/plugin/index.d.ts +40 -5
  25. package/dist/plugin/index.js +81 -8
  26. package/dist/plugin/strip-server-exports.d.ts +39 -0
  27. package/dist/plugin/strip-server-exports.js +258 -0
  28. package/dist/render.d.ts +1 -6
  29. package/dist/render.js +6 -40
  30. package/dist/router.js +14 -0
  31. package/dist/routes/scan.d.ts +6 -0
  32. package/dist/routes/scan.js +16 -4
  33. package/dist/runtime/dev-server.d.ts +3 -1
  34. package/dist/runtime/dev-server.js +56 -4
  35. package/dist/runtime/module-loader.d.ts +16 -1
  36. package/dist/runtime/module-loader.js +15 -1
  37. package/dist/runtime/template.js +11 -1
  38. package/dist/security/client-props.d.ts +9 -17
  39. package/dist/security/client-props.js +12 -31
  40. package/dist/security/config.d.ts +6 -9
  41. package/dist/security/config.js +5 -4
  42. package/dist/security/errors.d.ts +0 -11
  43. package/dist/security/errors.js +0 -21
  44. package/dist/security/index.d.ts +3 -3
  45. package/dist/security/index.js +2 -4
  46. package/dist/variants/assets.d.ts +1 -0
  47. package/dist/variants/assets.js +19 -1
  48. package/dist/variants/resolve.d.ts +1 -1
  49. package/dist/variants/resolve.js +3 -37
  50. package/package.json +4 -4
@@ -1,4 +1,13 @@
1
1
  import type { PlatformConfig } from './types';
2
+ /**
3
+ * Tipos generados a partir de lo que el proyecto declara.
4
+ *
5
+ * Sin esto, declarar un token de plataforma propio en `ress.config.ts` funciona
6
+ * en tiempo de ejecución pero el editor no lo conoce: el desarrollador escribe
7
+ * `'kaios'` y no recibe ni sugerencia ni error si lo escribe mal. Los archivos se
8
+ * generan, no se versionan, y se rehacen cuando cambia la configuración que los
9
+ * origina.
10
+ */
2
11
  /**
3
12
  * Amplía `PlatformValueMap` de `@ressjs/platform` con los tokens del proyecto.
4
13
  *
@@ -22,7 +31,7 @@ export declare function generateProjectConfigTypes(values: Record<string, unknow
22
31
  */
23
32
  export declare function writeGeneratedTypes(root: string, files: Record<string, string>): Promise<string[]>;
24
33
  /**
25
- * Genera `types/` para un proyecto cuya configuración ya se cargó con
34
+ * Genera `types/` sólo para proyectos con `tsconfig.json` cuya configuración se cargó con
26
35
  * `loadConfig`. La usan el plugin al arrancar y `ress info`, para que un
27
36
  * proyecto recién creado o un clon limpio tengan sus tipos sin levantar nada.
28
37
  */
@@ -13,8 +13,6 @@ exports.writeProjectTypes = writeProjectTypes;
13
13
  * generan, no se versionan, y se rehacen cuando cambia la configuración que los
14
14
  * origina.
15
15
  */
16
- const BANNER = '// Generado por ress.js a partir de la configuración del proyecto.\n' +
17
- '// No editar a mano ni versionar: se rehace en cada arranque.\n';
18
16
  /**
19
17
  * Amplía `PlatformValueMap` de `@ressjs/platform` con los tokens del proyecto.
20
18
  *
@@ -34,14 +32,13 @@ function generatePlatformTypes(platform) {
34
32
  if (byAxis.size === 0) {
35
33
  // Un archivo vacío igual se escribe: si el proyecto quitó sus tokens, dejar
36
34
  // el anterior mantendría en el autocompletado valores que ya no existen.
37
- return BANNER + 'export {}\n';
35
+ return 'export {}\n';
38
36
  }
39
37
  const fields = [...byAxis.entries()]
40
38
  .sort(([a], [b]) => a.localeCompare(b))
41
39
  .map(([axis, tokens]) => ` ${axis}: ${tokens.map(quote).join(' | ')}`)
42
40
  .join('\n');
43
- return (BANNER +
44
- "declare module '@ressjs/platform' {\n" +
41
+ return ("declare module '@ressjs/platform' {\n" +
45
42
  ' interface PlatformValueMap {\n' +
46
43
  fields +
47
44
  '\n }\n}\n\nexport {}\n');
@@ -55,10 +52,9 @@ function generatePlatformTypes(platform) {
55
52
  function generateProjectConfigTypes(values) {
56
53
  const keys = Object.keys(values).sort();
57
54
  if (keys.length === 0)
58
- return BANNER + 'export {}\n';
55
+ return 'export {}\n';
59
56
  const fields = keys.map((key) => ` ${propertyName(key)}: ${typeOf(values[key])}`).join('\n');
60
- return (BANNER +
61
- "declare module '@ressjs/config' {\n" +
57
+ return ("declare module '@ressjs/config' {\n" +
62
58
  ' interface ProjectConfig {\n' +
63
59
  fields +
64
60
  '\n }\n}\n\nexport {}\n');
@@ -118,11 +114,21 @@ async function writeGeneratedTypes(root, files) {
118
114
  return written;
119
115
  }
120
116
  /**
121
- * Genera `types/` para un proyecto cuya configuración ya se cargó con
117
+ * Genera `types/` sólo para proyectos con `tsconfig.json` cuya configuración se cargó con
122
118
  * `loadConfig`. La usan el plugin al arrancar y `ress info`, para que un
123
119
  * proyecto recién creado o un clon limpio tengan sus tipos sin levantar nada.
124
120
  */
125
121
  async function writeProjectTypes(root, platform) {
122
+ const { access } = await import('node:fs/promises');
123
+ const { join } = await import('node:path');
124
+ try {
125
+ await access(join(root, 'tsconfig.json'));
126
+ }
127
+ catch (error) {
128
+ if (error.code === 'ENOENT')
129
+ return [];
130
+ throw error;
131
+ }
126
132
  const { resolved } = await import('@ressjs/config');
127
133
  return writeGeneratedTypes(root, {
128
134
  'ress-platform.d.ts': generatePlatformTypes(platform),
@@ -13,6 +13,8 @@ export interface PublicEnvOptions {
13
13
  env?: Record<string, string | undefined>;
14
14
  /** Si faltar una variable detiene el arranque. En build, sí. */
15
15
  strict?: boolean;
16
+ /** Dónde se informa una variable ausente cuando `strict` es falso. Por defecto, la consola. */
17
+ warn?: (message: string) => void;
16
18
  }
17
19
  export declare function publicEnvDefines(publicEnv: readonly string[], options?: PublicEnvOptions): Record<string, string>;
18
20
  /** Los valores publicados, para inspeccionarlos o serializarlos. */
@@ -20,11 +20,14 @@ function publicEnvDefines(publicEnv, options = {}) {
20
20
  defines[`process.env.${name}`] = literal;
21
21
  defines[`import.meta.env.${name}`] = literal;
22
22
  }
23
- if (missing.length && options.strict !== false) {
24
- throw new Error(`[ress] \`publicEnv\` declara ${missing.length === 1 ? 'una variable que no existe' : 'variables que no existen'}: ` +
23
+ if (missing.length) {
24
+ const message = `[ress] \`publicEnv\` declara ${missing.length === 1 ? 'una variable que no existe' : 'variables que no existen'}: ` +
25
25
  missing.join(', ') +
26
26
  '. Exponer una variable inexistente es casi siempre un error de tipeo, y ' +
27
- 'el navegador recibiría el nombre sin valor.');
27
+ 'el navegador recibiría el nombre sin valor.';
28
+ if (options.strict !== false)
29
+ throw new Error(message);
30
+ (options.warn ?? console.warn)(message);
28
31
  }
29
32
  return defines;
30
33
  }
@@ -13,6 +13,6 @@ export type { CompiledRule, CompiledRules } from './rules';
13
13
  export { createConfigMiddleware } from './middleware';
14
14
  export { publicEnvDefines, publicEnvValues } from './env';
15
15
  export { generateProjectConfigTypes, generatePlatformTypes, writeGeneratedTypes, writeProjectTypes } from './codegen';
16
- export { changedKeys, isConfigLayer, restartReason, watchConfig, HOT_KEYS, RESTART_REASONS } from './watch';
16
+ export { changedKeys, isConfigLayer, restartReason, watchConfig, onConfigReload, HOT_KEYS, RESTART_REASONS } from './watch';
17
17
  export { validateConfig, validateRules } from './validate';
18
18
  export type { AssetsConfig, IsrConfig, ResolvedIsrConfig, HeaderRule, ModeOverrides, PlatformConfig, RedirectRule, ResolvedConfig, RessConfig, RessServerConfig, RewriteRule, TrailingSlashMode, } from './types';
@@ -7,7 +7,7 @@
7
7
  * resueltas por el mismo motor, y sólo la primera la lee el router.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.validateRules = exports.validateConfig = exports.RESTART_REASONS = exports.HOT_KEYS = exports.watchConfig = exports.restartReason = exports.isConfigLayer = exports.changedKeys = exports.writeProjectTypes = exports.writeGeneratedTypes = exports.generatePlatformTypes = exports.generateProjectConfigTypes = exports.publicEnvValues = exports.publicEnvDefines = exports.createConfigMiddleware = exports.parseRulePattern = exports.matchRule = exports.compileRules = exports.applyParams = exports.withBasePath = exports.stripBasePath = exports.canonicalTrailingSlash = exports.assetPrefixFor = exports.defineConfig = exports.resolveIsrConfig = exports.normalizeBasePath = exports.loadConfig = void 0;
10
+ exports.validateRules = exports.validateConfig = exports.RESTART_REASONS = exports.HOT_KEYS = exports.onConfigReload = exports.watchConfig = exports.restartReason = exports.isConfigLayer = exports.changedKeys = exports.writeProjectTypes = exports.writeGeneratedTypes = exports.generatePlatformTypes = exports.generateProjectConfigTypes = exports.publicEnvValues = exports.publicEnvDefines = exports.createConfigMiddleware = exports.parseRulePattern = exports.matchRule = exports.compileRules = exports.applyParams = exports.withBasePath = exports.stripBasePath = exports.canonicalTrailingSlash = exports.assetPrefixFor = exports.defineConfig = exports.resolveIsrConfig = exports.normalizeBasePath = exports.loadConfig = void 0;
11
11
  var load_1 = require("./load");
12
12
  Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return load_1.loadConfig; } });
13
13
  Object.defineProperty(exports, "normalizeBasePath", { enumerable: true, get: function () { return load_1.normalizeBasePath; } });
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "changedKeys", { enumerable: true, get: function
39
39
  Object.defineProperty(exports, "isConfigLayer", { enumerable: true, get: function () { return watch_1.isConfigLayer; } });
40
40
  Object.defineProperty(exports, "restartReason", { enumerable: true, get: function () { return watch_1.restartReason; } });
41
41
  Object.defineProperty(exports, "watchConfig", { enumerable: true, get: function () { return watch_1.watchConfig; } });
42
+ Object.defineProperty(exports, "onConfigReload", { enumerable: true, get: function () { return watch_1.onConfigReload; } });
42
43
  Object.defineProperty(exports, "HOT_KEYS", { enumerable: true, get: function () { return watch_1.HOT_KEYS; } });
43
44
  Object.defineProperty(exports, "RESTART_REASONS", { enumerable: true, get: function () { return watch_1.RESTART_REASONS; } });
44
45
  var validate_1 = require("./validate");
@@ -8,7 +8,6 @@ import type { IsrConfig, ResolvedConfig, ResolvedIsrConfig } from './types';
8
8
  * config/default.js ← lo común, opcional
9
9
  * config/development.js
10
10
  * config/production.js
11
- * config/test.js
12
11
  * config/local.js ← esta máquina; no se sube
13
12
  * ```
14
13
  *
@@ -14,7 +14,6 @@ const validate_1 = require("./validate");
14
14
  * config/default.js ← lo común, opcional
15
15
  * config/development.js
16
16
  * config/production.js
17
- * config/test.js
18
17
  * config/local.js ← esta máquina; no se sube
19
18
  * ```
20
19
  *
@@ -47,6 +46,8 @@ async function resolve(declared) {
47
46
  // obliga a recordar si ya está en cada concatenación.
48
47
  basePath: normalizeBasePath(declared.basePath ?? ''),
49
48
  trailingSlash: declared.trailingSlash ?? 'ignore',
49
+ // Sin barra inicial ni final: se compara contra rutas del proyecto.
50
+ shared: (declared.shared ?? []).map((entry) => entry.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')),
50
51
  // Las tres son funciones para que el proyecto pueda leer un archivo o
51
52
  // consultar algo al arrancar. Se invocan una vez, acá.
52
53
  redirects: (await declared.redirects?.()) ?? [],
@@ -29,6 +29,13 @@ export interface RessConfig {
29
29
  basePath?: string;
30
30
  /** Qué hacer con la barra final de una URL. */
31
31
  trailingSlash?: TrailingSlashMode;
32
+ /**
33
+ * Carpetas cuyo código carga Node una sola vez y comparten tu entrada de servidor
34
+ * (`server.js`, `api/`) y las páginas, relativas a la raíz del proyecto
35
+ * (`['app/services']`). Sin esto, un archivo importado por las dos partes se
36
+ * carga dos veces, y cada copia tiene su propio estado.
37
+ */
38
+ shared?: string[];
32
39
  redirects?: () => RedirectRule[] | Promise<RedirectRule[]>;
33
40
  rewrites?: () => RewriteRule[] | Promise<RewriteRule[]>;
34
41
  headers?: () => HeaderRule[] | Promise<HeaderRule[]>;
@@ -143,11 +150,6 @@ export interface PlatformConfig {
143
150
  }>;
144
151
  /** Plataformas para las que se prepara el proyecto generado. F-033. */
145
152
  targets?: string[];
146
- /**
147
- * Intención declarativa de usar clientes completos forkeables. Las plantillas
148
- * y sus runtimes pertenecen a F-028 (React Native) y F-029 (Electron).
149
- */
150
- forkedClients?: string[];
151
153
  }
152
154
  export interface RedirectRule {
153
155
  source: string;
@@ -176,6 +178,7 @@ export interface HeaderRule {
176
178
  export interface ResolvedConfig {
177
179
  basePath: string;
178
180
  trailingSlash: TrailingSlashMode;
181
+ shared: string[];
179
182
  redirects: RedirectRule[];
180
183
  rewrites: RewriteRule[];
181
184
  headers: HeaderRule[];
@@ -16,16 +16,26 @@ function validateConfig(config) {
16
16
  const problems = [];
17
17
  checkBasePath(config, problems);
18
18
  checkTrailingSlash(config, problems);
19
+ checkShared(config, problems);
19
20
  checkPublicEnv(config, problems);
20
21
  checkCallable(config, problems);
21
22
  checkAssets(config, problems);
22
23
  checkDev(config, problems);
23
24
  checkIsr(config, problems);
25
+ checkSecurity(config, problems);
26
+ checkPlatform(config, problems);
24
27
  if (problems.length === 0)
25
28
  return;
26
29
  throw new Error(`[ress] La configuración tiene ${problems.length === 1 ? 'un problema' : `${problems.length} problemas`}:\n` +
27
30
  problems.map((p) => ` · ${p}`).join('\n'));
28
31
  }
32
+ function checkPlatform({ platform }, problems) {
33
+ if (platform && Object.hasOwn(platform, 'forkedClients')) {
34
+ problems.push('`platform.forkedClients` se retiró en 0.6.0 porque no tenía efecto. ' +
35
+ 'Sacá esta clave; los clientes nativos viven en repositorios aparte. ' +
36
+ '`platform.targets` y las cabeceras de plataforma siguen funcionando.');
37
+ }
38
+ }
29
39
  function checkBasePath({ basePath }, problems) {
30
40
  if (basePath === undefined)
31
41
  return;
@@ -40,6 +50,21 @@ function checkBasePath({ basePath }, problems) {
40
50
  'otro dominio está `assets.cdnPrefix`.');
41
51
  }
42
52
  }
53
+ function checkShared({ shared }, problems) {
54
+ if (shared === undefined)
55
+ return;
56
+ if (!Array.isArray(shared)) {
57
+ return void problems.push('`shared` tiene que ser una lista de carpetas del proyecto (`[\'app/services\']`).');
58
+ }
59
+ for (const entry of shared) {
60
+ if (typeof entry !== 'string' || !entry.trim()) {
61
+ problems.push('`shared` sólo admite carpetas como cadenas no vacías.');
62
+ }
63
+ else if (/^([a-zA-Z]:)?[\\/]/.test(entry.trim()) || entry.split(/[\\/]/).includes('..')) {
64
+ problems.push(`\`shared\` lleva rutas relativas a la raíz del proyecto y dentro de ella; "${entry}" no lo es.`);
65
+ }
66
+ }
67
+ }
43
68
  function checkTrailingSlash({ trailingSlash }, problems) {
44
69
  const valid = ['always', 'never', 'ignore'];
45
70
  if (trailingSlash !== undefined && !valid.includes(trailingSlash)) {
@@ -78,6 +103,48 @@ function checkAssets({ assets }, problems) {
78
103
  'los artefactos se sirven desde otro origen.');
79
104
  }
80
105
  }
106
+ /**
107
+ * `security` se resuelve después, con sus valores por defecto, pero un valor de
108
+ * tipo equivocado no tiene que llegar hasta ahí: una cabecera con `undefined`
109
+ * o un límite negativo falla en cada petición en vez de al arrancar.
110
+ */
111
+ function checkSecurity({ security }, problems) {
112
+ if (security === undefined)
113
+ return;
114
+ if (typeof security !== 'object' || security === null) {
115
+ return void problems.push('`security` tiene que ser un objeto.');
116
+ }
117
+ const s = security;
118
+ const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
119
+ const isBoolean = (v) => typeof v === 'boolean';
120
+ const isString = (v) => typeof v === 'string';
121
+ const isNonNegativeInteger = (v) => Number.isInteger(v) && v >= 0;
122
+ const isStringList = (v) => Array.isArray(v) && v.every((item) => typeof item === 'string');
123
+ const oneOf = (...values) => (v) => values.includes(v);
124
+ const orFalse = (check) => (v) => v === false || check(v);
125
+ const rules = [
126
+ ['security.maxStateBytes', s.maxStateBytes, isNonNegativeInteger, 'un entero no negativo'],
127
+ ['security.onStateOverLimit', s.onStateOverLimit, oneOf('warn', 'error'), "'warn' o 'error'"],
128
+ ['security.serializePlatform', s.serializePlatform, oneOf('auto', 'always', 'never'), "'auto', 'always' o 'never'"],
129
+ ];
130
+ const groups = ['headers', 'cache', 'csp', 'head', 'dev'];
131
+ for (const group of groups) {
132
+ if (s[group] !== undefined && !object(s[group])) {
133
+ problems.push(`\`security.${group}\` tiene que ser un objeto.`);
134
+ continue;
135
+ }
136
+ }
137
+ const h = object(s.headers) ? s.headers : {};
138
+ const c = object(s.cache) ? s.cache : {};
139
+ const csp = object(s.csp) ? s.csp : {};
140
+ const head = object(s.head) ? s.head : {};
141
+ const dev = object(s.dev) ? s.dev : {};
142
+ rules.push(['security.headers.contentTypeOptions', h.contentTypeOptions, isBoolean, 'un booleano'], ['security.headers.referrerPolicy', h.referrerPolicy, orFalse(isString), 'una cadena o false'], ['security.headers.frameOptions', h.frameOptions, orFalse(oneOf('DENY', 'SAMEORIGIN')), "'DENY', 'SAMEORIGIN' o false"], ['security.headers.hsts', h.hsts, orFalse((v) => object(v) && isNonNegativeInteger(v.maxAge)), 'false o un objeto con `maxAge` entero'], ['security.cache.html', c.html, isString, 'una cadena'], ['security.cache.immutableMaxAge', c.immutableMaxAge, isNonNegativeInteger, 'un entero no negativo'], ['security.cache.staticMaxAge', c.staticMaxAge, isNonNegativeInteger, 'un entero no negativo'], ['security.csp.enabled', csp.enabled, isBoolean, 'un booleano'], ['security.csp.reportOnly', csp.reportOnly, isBoolean, 'un booleano'], ['security.csp.reportUri', csp.reportUri, isString, 'una cadena'], ['security.csp.directives', csp.directives, (v) => object(v) && Object.values(v).every(orFalse(isStringList)), 'un objeto cuyos valores son listas de cadenas o false'], ['security.head.allowScriptTags', head.allowScriptTags, isBoolean, 'un booleano'], ['security.head.extraTagAllowlist', head.extraTagAllowlist, isStringList, 'una lista de cadenas'], ['security.dev.allowedHosts', dev.allowedHosts, isStringList, 'una lista de cadenas'], ['security.dev.allowedOrigins', dev.allowedOrigins, orFalse(isStringList), 'false o una lista de cadenas']);
143
+ for (const [key, value, valid, expected] of rules) {
144
+ if (value !== undefined && !valid(value))
145
+ problems.push(`\`${key}\` tiene que ser ${expected}.`);
146
+ }
147
+ }
81
148
  function checkIsr({ isr }, problems) {
82
149
  if (isr === undefined)
83
150
  return;
@@ -36,6 +36,21 @@ export declare function changedKeys(previous: ResolvedConfig, next: ResolvedConf
36
36
  export declare function manualRestartReason(changed: readonly string[]): string | undefined;
37
37
  /** El motivo del reinicio, o nada si todo lo que cambió se aplica en caliente. */
38
38
  export declare function restartReason(changed: readonly string[]): string | undefined;
39
+ /**
40
+ * Espera a que un archivo deje de cambiar.
41
+ *
42
+ * Guardar es truncar y escribir, y el sistema puede avisar entre un paso y el otro
43
+ * (Windows lo hace, y no siempre vuelve a avisar al terminar). Leer en ese momento
44
+ * da un archivo vacío: la configuración sale por defecto, no hay diferencias ni
45
+ * error, y el cambio se pierde. Se mira el tamaño y la fecha hasta que dos
46
+ * lecturas seguidas coinciden. Un archivo vacío cuenta como «todavía no
47
+ * terminó»: casi siempre es el paso intermedio de una escritura. Si de verdad
48
+ * está vacío, se lo lee igual cuando se agotan los intentos.
49
+ */
50
+ export declare function settled(file: string, { interval, attempts }?: {
51
+ interval?: number | undefined;
52
+ attempts?: number | undefined;
53
+ }): Promise<void>;
39
54
  /** ¿Este archivo es una capa de configuración del proyecto? */
40
55
  export declare function isConfigLayer(file: string, root: string): boolean;
41
56
  export interface WatchConfigOptions {
@@ -49,5 +64,12 @@ export interface WatchConfigOptions {
49
64
  onRestart?: (reason: string) => void;
50
65
  log?: (message: string) => void;
51
66
  }
67
+ /**
68
+ * Quien arma el pipeline de petición (el router) se entera acá de una recarga en
69
+ * caliente. El plugin de Vite y el router viven en objetos distintos: sin este
70
+ * aviso, la configuración nueva llegaba al plugin y las peticiones seguían
71
+ * usando las reglas con las que arrancó el servidor. Devuelve la baja.
72
+ */
73
+ export declare function onConfigReload(listener: (config: ResolvedConfig) => void): () => void;
52
74
  /** Conecta el observador de Vite con la carpeta de configuración. */
53
75
  export declare function watchConfig(server: any, options: WatchConfigOptions): void;
@@ -4,9 +4,13 @@ exports.MANUAL_RESTART_KEYS = exports.RESTART_REASONS = exports.HOT_KEYS = void
4
4
  exports.changedKeys = changedKeys;
5
5
  exports.manualRestartReason = manualRestartReason;
6
6
  exports.restartReason = restartReason;
7
+ exports.settled = settled;
7
8
  exports.isConfigLayer = isConfigLayer;
9
+ exports.onConfigReload = onConfigReload;
8
10
  exports.watchConfig = watchConfig;
11
+ const promises_1 = require("node:fs/promises");
9
12
  const config_1 = require("@ressjs/config");
13
+ const codegen_1 = require("./codegen");
10
14
  const load_1 = require("./load");
11
15
  /**
12
16
  * Recarga de la configuración en desarrollo.
@@ -33,6 +37,7 @@ exports.HOT_KEYS = ['redirects', 'rewrites', 'headers', 'trailingSlash'];
33
37
  */
34
38
  exports.RESTART_REASONS = {
35
39
  basePath: 'cambia la URL bajo la que se sirve todo, incluidos los artefactos ya emitidos',
40
+ shared: 'Node carga esas carpetas una sola vez, al arrancar, y las comparten tu entrada de servidor y las páginas',
36
41
  extensions: 'cambia qué archivos son páginas, y eso redefine el catálogo de entradas',
37
42
  platform: 'redefine los ejes de plataforma, y con ellos las variantes que se construyen',
38
43
  publicEnv: 'las variables se reemplazan en tiempo de build, no al atender una petición',
@@ -55,6 +60,7 @@ exports.RESTART_REASONS = {
55
60
  */
56
61
  exports.MANUAL_RESTART_KEYS = new Set([
57
62
  'basePath',
63
+ 'shared',
58
64
  'assets',
59
65
  'security',
60
66
  'isr',
@@ -110,6 +116,36 @@ function restartReason(changed) {
110
116
  }
111
117
  return undefined;
112
118
  }
119
+ /**
120
+ * Espera a que un archivo deje de cambiar.
121
+ *
122
+ * Guardar es truncar y escribir, y el sistema puede avisar entre un paso y el otro
123
+ * (Windows lo hace, y no siempre vuelve a avisar al terminar). Leer en ese momento
124
+ * da un archivo vacío: la configuración sale por defecto, no hay diferencias ni
125
+ * error, y el cambio se pierde. Se mira el tamaño y la fecha hasta que dos
126
+ * lecturas seguidas coinciden. Un archivo vacío cuenta como «todavía no
127
+ * terminó»: casi siempre es el paso intermedio de una escritura. Si de verdad
128
+ * está vacío, se lo lee igual cuando se agotan los intentos.
129
+ */
130
+ async function settled(file, { interval = 30, attempts = 30 } = {}) {
131
+ const signature = async () => {
132
+ try {
133
+ const { size, mtimeMs } = await (0, promises_1.stat)(file);
134
+ return `${size}:${mtimeMs}`;
135
+ }
136
+ catch {
137
+ return 'ausente';
138
+ }
139
+ };
140
+ let previous = await signature();
141
+ for (let attempt = 0; attempt < attempts; attempt++) {
142
+ await new Promise((resolve) => setTimeout(resolve, interval));
143
+ const current = await signature();
144
+ if (current === previous && !current.startsWith('0:'))
145
+ return;
146
+ previous = current;
147
+ }
148
+ }
113
149
  /** ¿Este archivo es una capa de configuración del proyecto? */
114
150
  function isConfigLayer(file, root) {
115
151
  const normalized = file.replace(/\\/g, '/');
@@ -119,6 +155,17 @@ function isConfigLayer(file, root) {
119
155
  const relative = normalized.slice(base.length + 1);
120
156
  return /^config\/[^/]+$/.test(relative) || /^config\.[^/]+$/.test(relative);
121
157
  }
158
+ const reloadListeners = new Set();
159
+ /**
160
+ * Quien arma el pipeline de petición (el router) se entera acá de una recarga en
161
+ * caliente. El plugin de Vite y el router viven en objetos distintos: sin este
162
+ * aviso, la configuración nueva llegaba al plugin y las peticiones seguían
163
+ * usando las reglas con las que arrancó el servidor. Devuelve la baja.
164
+ */
165
+ function onConfigReload(listener) {
166
+ reloadListeners.add(listener);
167
+ return () => void reloadListeners.delete(listener);
168
+ }
122
169
  /** Conecta el observador de Vite con la carpeta de configuración. */
123
170
  function watchConfig(server, options) {
124
171
  const log = options.log ?? ((message) => console.log(message));
@@ -130,6 +177,8 @@ function watchConfig(server, options) {
130
177
  const onChange = async (file) => {
131
178
  if (!isConfigLayer(file, options.root))
132
179
  return;
180
+ // Se lee el archivo terminado, no el que se está escribiendo.
181
+ await settled(file);
133
182
  // Lo resuelto se descarta entero: la fusión depende de todas las capas, así
134
183
  // que un cambio en cualquiera de ellas invalida el resultado.
135
184
  (0, config_1.invalidateConfig)();
@@ -143,6 +192,12 @@ function watchConfig(server, options) {
143
192
  log(`[ress] La configuración no se pudo cargar: ${error.message}`);
144
193
  return;
145
194
  }
195
+ // Los tipos de `config.X` salen de lo resuelto, valores propios incluidos, y
196
+ // un valor propio nuevo no figura en `changedKeys` (sólo compara las claves
197
+ // del framework): se regeneran con cada cambio de capa.
198
+ await (0, codegen_1.writeProjectTypes)(options.root, next.platform).catch((error) => {
199
+ log(`[ress] No se pudieron regenerar los tipos de la configuración: ${error.message}`);
200
+ });
146
201
  const changed = changedKeys(options.current(), next);
147
202
  if (changed.length === 0)
148
203
  return;
@@ -155,6 +210,8 @@ function watchConfig(server, options) {
155
210
  if (reason)
156
211
  return restart(reason);
157
212
  options.onReload(next);
213
+ for (const listener of reloadListeners)
214
+ listener(next);
158
215
  log(`[ress] Configuración recargada: ${changed.join(', ')}`);
159
216
  };
160
217
  for (const event of ['add', 'change', 'unlink']) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * La ruta real de un archivo o directorio, sin enlaces simbólicos.
3
+ *
4
+ * El bundler informa cada módulo con su ruta real. Si la raíz del proyecto se
5
+ * toma tal como la escribió el usuario —a través de un enlace, como `/tmp` en
6
+ * macOS—, comparar una ruta con la otra falla sin decir por qué. La raíz se
7
+ * canoniza una vez, donde se resuelve, y todo lo que se derive de ella coincide.
8
+ *
9
+ * Una ruta que no existe se devuelve resuelta pero sin canonizar: el error de
10
+ * que no existe le corresponde a quien la use.
11
+ */
12
+ export declare function canonicalPath(target: string): string;
@@ -0,0 +1,28 @@
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.canonicalPath = canonicalPath;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * La ruta real de un archivo o directorio, sin enlaces simbólicos.
11
+ *
12
+ * El bundler informa cada módulo con su ruta real. Si la raíz del proyecto se
13
+ * toma tal como la escribió el usuario —a través de un enlace, como `/tmp` en
14
+ * macOS—, comparar una ruta con la otra falla sin decir por qué. La raíz se
15
+ * canoniza una vez, donde se resuelve, y todo lo que se derive de ella coincide.
16
+ *
17
+ * Una ruta que no existe se devuelve resuelta pero sin canonizar: el error de
18
+ * que no existe le corresponde a quien la use.
19
+ */
20
+ function canonicalPath(target) {
21
+ const absolute = node_path_1.default.resolve(target);
22
+ try {
23
+ return node_fs_1.realpathSync.native(absolute);
24
+ }
25
+ catch {
26
+ return absolute;
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Carpetas compartidas con Node (`shared` en la config).
3
+ *
4
+ * Un archivo importado por la entrada de servidor (`server.js`, `api/`) y por las
5
+ * páginas se carga dos veces si cada parte lo carga por su cuenta: Node, y el runner
6
+ * de Vite en desarrollo o el bundle compilado en producción. Cada copia tiene su
7
+ * propio estado. Los archivos de estas carpetas los carga siempre Node, así que hay
8
+ * una sola.
9
+ */
10
+ /** ¿Este archivo cae dentro de una carpeta compartida? `root` es la raíz del proyecto. */
11
+ export declare function isSharedModule(id: string, root: string, shared: readonly string[]): boolean;
12
+ /** Marca con la que el build deja un import compartido hasta que se conoce la ubicación del chunk. */
13
+ export declare const SHARED_PREFIX = "ress-shared:";
14
+ /**
15
+ * Reescribe los marcadores de un chunk por la ruta relativa real al archivo.
16
+ *
17
+ * Se hace al terminar de armar el chunk porque sólo entonces se sabe dónde queda: la
18
+ * ruta «relativa» que calcula el bundler parte de la fuente de la entrada, no del
19
+ * archivo de salida. La app se despliega con `dist/` y las carpetas compartidas
20
+ * lado a lado, así que la ruta relativa sigue valiendo al moverla de lugar.
21
+ */
22
+ export declare function resolveSharedMarkers(code: string, chunkFile: string, root: string): string;
@@ -0,0 +1,44 @@
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.SHARED_PREFIX = void 0;
7
+ exports.isSharedModule = isSharedModule;
8
+ exports.resolveSharedMarkers = resolveSharedMarkers;
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ /**
11
+ * Carpetas compartidas con Node (`shared` en la config).
12
+ *
13
+ * Un archivo importado por la entrada de servidor (`server.js`, `api/`) y por las
14
+ * páginas se carga dos veces si cada parte lo carga por su cuenta: Node, y el runner
15
+ * de Vite en desarrollo o el bundle compilado en producción. Cada copia tiene su
16
+ * propio estado. Los archivos de estas carpetas los carga siempre Node, así que hay
17
+ * una sola.
18
+ */
19
+ /** ¿Este archivo cae dentro de una carpeta compartida? `root` es la raíz del proyecto. */
20
+ function isSharedModule(id, root, shared) {
21
+ const file = id.split('?')[0].replace(/\\/g, '/');
22
+ const base = root.replace(/\\/g, '/').replace(/\/+$/, '');
23
+ return shared.some((entry) => file.startsWith(`${base}/${entry}/`));
24
+ }
25
+ /** Marca con la que el build deja un import compartido hasta que se conoce la ubicación del chunk. */
26
+ exports.SHARED_PREFIX = 'ress-shared:';
27
+ /**
28
+ * Reescribe los marcadores de un chunk por la ruta relativa real al archivo.
29
+ *
30
+ * Se hace al terminar de armar el chunk porque sólo entonces se sabe dónde queda: la
31
+ * ruta «relativa» que calcula el bundler parte de la fuente de la entrada, no del
32
+ * archivo de salida. La app se despliega con `dist/` y las carpetas compartidas
33
+ * lado a lado, así que la ruta relativa sigue valiendo al moverla de lugar.
34
+ */
35
+ function resolveSharedMarkers(code, chunkFile, root) {
36
+ if (!code.includes(exports.SHARED_PREFIX))
37
+ return code;
38
+ return code.replace(new RegExp(`${exports.SHARED_PREFIX}([^"'\`\\s)]+)`, 'g'), (_match, rel) => {
39
+ const relative = node_path_1.default
40
+ .relative(node_path_1.default.dirname(chunkFile), node_path_1.default.join(root, rel))
41
+ .replace(/\\/g, '/');
42
+ return relative.startsWith('.') ? relative : `./${relative}`;
43
+ });
44
+ }
@@ -45,11 +45,8 @@ function buildRenderServer(config) {
45
45
  const resolvedViteConfig = typeof config.viteConfig === 'function'
46
46
  ? config.viteConfig()
47
47
  : config.viteConfig;
48
- const options = config.security ?? (0, security_1.resolveSecurityOptions)(config.securityOptions);
49
48
  return (0, render_1.createRenderServer)({
50
49
  ...(resolvedViteConfig.moduleLoader ? { moduleLoader: resolvedViteConfig.moduleLoader } : {}),
51
- clientProps: options.clientProps,
52
- isProduction: resolvedViteConfig.isProduction,
53
50
  });
54
51
  }
55
52
  async function processRequest(context, config, renderServer) {
@@ -112,16 +109,11 @@ async function processRequest(context, config, renderServer) {
112
109
  if (!res.locals.styles && page?.pageId && viteConfig.catalog && viteConfig.assets) {
113
110
  res.locals.styles = (0, assets_1.resolvePageAssets)(page.pageId, platformContext.platformInfo ?? platform_1.DEFAULT_PLATFORM, viteConfig.catalog, viteConfig.assets);
114
111
  }
115
- const pageLocals = { ...res.locals };
116
- delete pageLocals.styles;
117
112
  const rendered = await renderServer({
118
113
  url: pathname,
119
114
  req,
120
115
  res,
121
116
  props: res.locals.serverSideProps,
122
- // `styles` pertenece al pipeline interno de assets; no se mezcla con los
123
- // locals que la página recibe ni se considera estado serializable.
124
- locals: pageLocals,
125
117
  hydrate,
126
118
  // La página ya la resolvió el despachador: buscarla de nuevo por URL sería
127
119
  // una segunda resolución con otro criterio.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { getViteSSRInput } from './pages';
3
3
  export { createRouteManifest, serializeRouteManifest, loadRouteManifest } from './routes/manifest';
4
4
  export { resolveServerModule } from './routes/module';
5
5
  export type { ResolveServerModuleContext } from './routes/module';
6
- export { scanRoutes } from './routes/scan';
6
+ export { scanRoutes, RouteConflictError } from './routes/scan';
7
7
  export type { PageId, RouteEntry, RouteEntryKind, RouteManifest, RouteManifestData, RouteMatch, RouteSegment, SegmentKind, } from './routes/types';
8
8
  export { ROUTE_MANIFEST_VERSION } from './routes/types';
9
9
  export { defineEnvironments, OUT_DIR } from './plugin/environments';
@@ -14,26 +14,27 @@ export type { FetchableEnvironment, ModuleLoader } from './runtime/module-loader
14
14
  export { createRuntime, resolveServerModulePath, resolveProductionModuleId, } from './runtime/prod-server';
15
15
  export { resolveSsrEnvironment } from './runtime/dev-server';
16
16
  export type { DevRuntimeOptions, RessRuntime } from './runtime/dev-server';
17
- export { DEFAULT_EXCLUSIONS, isPageFile, pageExclusionOf, scriptExtensionOf, SCRIPT_EXTENSIONS, } from './fs/module-extensions';
17
+ export { DEFAULT_EXCLUSIONS, DEFAULT_EXTENSION_POLICY, isPageFile, pageExclusionOf, scriptExtensionOf, SCRIPT_EXTENSIONS, setExtensionPolicy, } from './fs/module-extensions';
18
18
  export type { PageExclusion } from './fs/module-extensions';
19
19
  export { buildVariantCatalog, pageIdFromFile } from './variants/catalog';
20
20
  export { resolveVariant, explainResolution } from './variants/resolve';
21
21
  export { defaultPlatformRegistry } from './variants/platform-tokens';
22
22
  export type { VariantCatalog, BuildEntry, Variant, AssetIndex, PlatformRegistry } from './variants/types';
23
23
  export { createRenderServer } from './render';
24
+ export { canonicalPath } from './fs/canonical-path';
24
25
  export { detectPlatform } from './platform';
25
26
  export type { PlatformInfo } from './platform';
26
27
  export type { ViteRouterOptions } from './router';
27
28
  export type { PageInfo } from './pages';
28
29
  export type { PageConfig, PageConfigVariant, PageHeadConfig, PageRenderingConfig, RenderingMode, RobotsConfig, } from './head/types';
29
30
  export type { ServerSideResult } from './helpers/middlewares';
30
- export { PublicFacingError, resolveSecurityOptions, serializeState, applySecurityHeaders, } from './security';
31
- export type { ClientPropsPolicy, PublicError, ResolvedSecurityOptions, SecurityOptions, } from './security';
31
+ export { resolveSecurityOptions, serializeState, applySecurityHeaders, } from './security';
32
+ export type { PublicError, ResolvedSecurityOptions, SecurityOptions, } from './security';
32
33
  /**
33
34
  * @deprecated Usá el manifest de rutas: `runtime.routes.entries`. Devuelve las
34
35
  * páginas tal como las enumera el catálogo de variantes, que no conoce grupos ni
35
36
  * catch-all. Se elimina cuando F-016 consuma el manifest.
36
37
  */
37
38
  export { getPages as listDetectedPages } from './pages';
38
- export { defineConfig, loadConfig, createConfigMiddleware, compileRules, publicEnvDefines, withBasePath, assetPrefixFor, writeProjectTypes, } from './config';
39
+ export { defineConfig, loadConfig, normalizeBasePath, validateConfig, createConfigMiddleware, compileRules, publicEnvDefines, withBasePath, assetPrefixFor, writeProjectTypes, } from './config';
39
40
  export type { RessConfig, ResolvedConfig, TrailingSlashMode, RessServerConfig } from './config';