@ressjs/cli 0.6.0-experimental.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.
package/LICENSE ADDED
@@ -0,0 +1,69 @@
1
+ ress.js Proprietary License
2
+
3
+ Copyright (c) 2025-2026 gabrielspisso. All rights reserved.
4
+
5
+ This license applies to the ress.js packages (the "Software"), published on npm
6
+ under the @ressjs and @gzzy scopes, in every version released from 0.6.0-rc.0
7
+ onwards. Earlier versions were released under the MIT License and remain
8
+ available under those terms.
9
+
10
+ 1. GRANT
11
+ Subject to this license, the Licensor grants you a worldwide, non-exclusive,
12
+ non-transferable, non-sublicensable, royalty-free license to install and use
13
+ the Software, as distributed by the Licensor, to build, run and deploy your
14
+ own applications and services, including commercial ones, and to charge your
15
+ own customers for them.
16
+
17
+ 2. RESTRICTIONS
18
+ You may not:
19
+ a) fork, copy, or redistribute the Software, in whole or in part, except for
20
+ the portions that are necessarily included in the build output of your own
21
+ application in order to run it;
22
+ b) publish, distribute or make available a modified version of the Software
23
+ or any work derived from it. Modifications for your own internal use are
24
+ allowed, but they remain subject to this license;
25
+ c) use the Software, or any part of it, to build, offer or support a
26
+ framework, library, tool or service that competes with the Software;
27
+ d) sell, sublicense, rent or host the Software itself as a product or service
28
+ for third parties, as opposed to hosting your own application built with
29
+ it;
30
+ e) remove or alter copyright, license or attribution notices;
31
+ f) use the name "ress.js" or the Licensor's names or marks to endorse or
32
+ promote your products without written permission.
33
+
34
+ 3. SCAFFOLDING OUTPUT
35
+ Files that @ressjs/create-ress-app copies into your project (templates and
36
+ generated configuration) are yours: you may use, modify and distribute them
37
+ without restriction. This does not extend to the Software packages that the
38
+ generated project installs as dependencies.
39
+
40
+ 4. SUPPORT
41
+ The Software is provided without any obligation of support, maintenance or
42
+ updates. The Licensor may offer support, maintenance, training or consulting
43
+ under a separate paid agreement; that agreement does not change this license
44
+ unless it says so explicitly and is signed by the Licensor.
45
+
46
+ 5. OWNERSHIP
47
+ The Software is licensed, not sold. The Licensor retains all rights not
48
+ expressly granted here. Feedback and suggestions you send may be used by the
49
+ Licensor without obligation to you.
50
+
51
+ 6. TERMINATION
52
+ This license ends automatically if you breach it. On termination you must
53
+ stop using the Software and delete your copies. Sections 4 to 8 survive
54
+ termination.
55
+
56
+ 7. NO WARRANTY
57
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
58
+ IMPLIED, INCLUDING THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
59
+ PARTICULAR PURPOSE AND NON-INFRINGEMENT.
60
+
61
+ 8. LIMITATION OF LIABILITY
62
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR WILL NOT BE LIABLE FOR
63
+ ANY INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES, OR FOR LOSS OF
64
+ PROFITS, DATA OR BUSINESS, ARISING FROM THE USE OF OR INABILITY TO USE THE
65
+ SOFTWARE. THE LICENSOR'S TOTAL LIABILITY IS LIMITED TO THE AMOUNT YOU PAID
66
+ FOR THE SOFTWARE, WHICH MAY BE ZERO.
67
+
68
+ For other uses, including exceptions to section 2, contact the Licensor to
69
+ agree a separate license.
package/dist/api/app.js CHANGED
@@ -23,6 +23,8 @@ import { resolveOptions } from './options.js';
23
23
  import { silentReporter } from './reporter.js';
24
24
  import { RessError } from './errors.js';
25
25
  import { applyServerFile } from './extend.js';
26
+ import { serverEntryOptions } from './server-context.js';
27
+ import { toViteLogLevel } from './limits.js';
26
28
  /** Cierra un `http.Server`, tolerando que ya esté cerrado. Idempotente. */
27
29
  function closeHttpServer(server) {
28
30
  if (!server.listening)
@@ -49,6 +51,7 @@ async function assertBuildPresent(root, outDir) {
49
51
  }
50
52
  }
51
53
  export async function createRessApp(opts = {}) {
54
+ opts = { ...serverEntryOptions.getStore(), ...opts };
52
55
  const configStart = Date.now();
53
56
  const reporter = opts.reporter ?? silentReporter;
54
57
  // El host por defecto difiere por modo: en desarrollo, sólo esta máquina; en
@@ -66,7 +69,8 @@ export async function createRessApp(opts = {}) {
66
69
  const isProduction = options.mode.value === 'production';
67
70
  const root = options.root.value;
68
71
  if (isProduction) {
69
- await assertBuildPresent(root, options.outDir.value);
72
+ // La que el pipeline realmente escribe y sirve: `--out-dir` todavía no cambia eso.
73
+ await assertBuildPresent(root, OUT_DIR.base);
70
74
  }
71
75
  const app = express();
72
76
  // Vía de escape nivel 1: antes del enrutador del framework, gana ante sus rutas.
@@ -84,6 +88,7 @@ export async function createRessApp(opts = {}) {
84
88
  // diseño, ya no existe en el proyecto (UC-1, UC-6).
85
89
  const runtime = await createRuntime(isProduction, options.base.value, assetPrefix, {
86
90
  plugins: [react(), ressPlugin()],
91
+ logLevel: toViteLogLevel(options.logLevel.value),
87
92
  });
88
93
  const router = createViteRouter({
89
94
  isProduction,
@@ -150,6 +155,14 @@ export async function createRessApp(opts = {}) {
150
155
  server.once('listening', onListening);
151
156
  server.listen(port, host);
152
157
  });
158
+ const closeApp = async () => {
159
+ if (closed)
160
+ return;
161
+ closed = true;
162
+ if (httpServer)
163
+ await closeHttpServer(httpServer);
164
+ await runtime.close();
165
+ };
153
166
  return {
154
167
  app,
155
168
  options,
@@ -161,7 +174,12 @@ export async function createRessApp(opts = {}) {
161
174
  const server = http.createServer(app);
162
175
  httpServer = server;
163
176
  const host = options.host.value;
164
- const boundPort = await bindPort(server, host, options.port.value);
177
+ // Si el bind falla, Vite (watchers y websocket de HMR) ya está creado: sin
178
+ // cerrarlo, el proceso imprime el error y queda vivo en vez de salir.
179
+ const boundPort = await bindPort(server, host, options.port.value).catch(async (error) => {
180
+ await closeApp().catch(() => { });
181
+ throw error;
182
+ });
165
183
  const displayHost = host === '0.0.0.0' ? 'localhost' : host;
166
184
  const url = `http://${displayHost}:${boundPort}${options.base.value}`;
167
185
  reporter.event({ type: 'server:listening', url, mode: options.mode.value, port: boundPort });
@@ -176,14 +194,7 @@ export async function createRessApp(opts = {}) {
176
194
  close: () => closeHttpServer(server),
177
195
  };
178
196
  },
179
- async close() {
180
- if (closed)
181
- return;
182
- closed = true;
183
- if (httpServer)
184
- await closeHttpServer(httpServer);
185
- await runtime.close();
186
- },
197
+ close: closeApp,
187
198
  };
188
199
  }
189
200
  export { OUT_DIR };
package/dist/api/build.js CHANGED
@@ -18,10 +18,11 @@
18
18
  import path from 'node:path';
19
19
  import { readdir, rm, stat } from 'node:fs/promises';
20
20
  import react from '@vitejs/plugin-react';
21
- import { getPages, buildVariantCatalog, OUT_DIR, ressPlugin, scanRoutes } from '@ressjs/vite-router';
21
+ import { getPages, buildVariantCatalog, OUT_DIR, ressPlugin, scanRoutes, RouteConflictError } from '@ressjs/vite-router';
22
22
  import { resolveOptions } from './options.js';
23
23
  import { silentReporter } from './reporter.js';
24
24
  import { RessError } from './errors.js';
25
+ import { outDirIgnored, toViteLogLevel } from './limits.js';
25
26
  const TARGET_DIRS = {
26
27
  client: OUT_DIR.client,
27
28
  ssr: OUT_DIR.ssr,
@@ -35,8 +36,7 @@ export async function buildApp(opts = {}) {
35
36
  if (opts.outDir && opts.outDir !== OUT_DIR.base) {
36
37
  reporter.event({
37
38
  type: 'warn',
38
- message: `--out-dir "${opts.outDir}" se registró, pero el pipeline de build (F-015/F-016/F-018) ` +
39
- `siempre escribe en "${OUT_DIR.base}". Es una limitación conocida, no un bug silencioso.`,
39
+ message: outDirIgnored(opts.outDir, OUT_DIR.base),
40
40
  });
41
41
  }
42
42
  // Igual que el resto de los comandos: abortar antes de tocar el disco si hay
@@ -45,9 +45,11 @@ export async function buildApp(opts = {}) {
45
45
  await scanRoutes({ strict: true, pagesDir: path.join(root, 'app/pages') });
46
46
  }
47
47
  catch (cause) {
48
- const message = cause instanceof Error ? cause.message : String(cause);
49
- const files = [...message.matchAll(/"([^"]+)"/g)].map((m) => m[1]).slice(0, 2);
50
- throw RessError.routeConflict(message, files);
48
+ // Sólo el conflicto de rutas se traduce; cualquier otro error del escaneo
49
+ // se propaga tal cual, en vez de presentarse como un conflicto.
50
+ if (cause instanceof RouteConflictError)
51
+ throw RessError.routeConflict(cause.pattern, [...cause.files]);
52
+ throw cause;
51
53
  }
52
54
  if (clean) {
53
55
  await rm(path.join(root, OUT_DIR.base), { recursive: true, force: true });
@@ -55,7 +57,11 @@ export async function buildApp(opts = {}) {
55
57
  const { createBuilder } = await import('vite');
56
58
  const builder = await createBuilder({
57
59
  root,
58
- mode: 'production',
60
+ // El modo efectivo (`--mode`, por defecto `production`): es el que el plugin usa
61
+ // para elegir la capa de configuración. Fijo en `production`, `--mode development`
62
+ // calculaba las opciones con una capa y construía con otra.
63
+ mode: options.mode.value,
64
+ logLevel: toViteLogLevel(options.logLevel.value),
59
65
  plugins: [react(), ressPlugin({ extensions: options.config.extensions })],
60
66
  build: { emptyOutDir: false },
61
67
  });
package/dist/api/dev.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { createRessApp } from './app.js';
2
+ import { devModeIgnored } from './limits.js';
3
+ import { silentReporter } from './reporter.js';
2
4
  /** `ress dev`: construye la app en modo desarrollo y la pone a escuchar. */
3
5
  export async function startDevServer(opts = {}) {
6
+ if (opts.mode !== undefined && opts.mode !== 'development') {
7
+ ;
8
+ (opts.reporter ?? silentReporter).event({ type: 'warn', message: devModeIgnored(opts.mode) });
9
+ }
4
10
  const app = await createRessApp({ ...opts, mode: 'development' });
5
11
  return app.listen();
6
12
  }
@@ -6,7 +6,25 @@
6
6
  * excepción del código del usuario, un fallo de compilación— se presenta con su
7
7
  * traza tal cual, porque ocultarla sería peor.
8
8
  */
9
- export type RessErrorCode = 'RESS_CONFIG_INVALID' | 'RESS_CONFIG_NOT_FOUND' | 'RESS_ROUTE_CONFLICT' | 'RESS_ROUTE_PATTERN_INVALID' | 'RESS_BUILD_MISSING' | 'RESS_BUILD_INCOMPATIBLE' | 'RESS_PORT_IN_USE' | 'RESS_SERVER_FILE_INVALID' | 'RESS_SERVER_FILE_THREW' | 'RESS_BAD_USAGE';
9
+ export type RessErrorCode = 'RESS_CONFIG_INVALID' | 'RESS_CONFIG_NOT_FOUND' | 'RESS_ROUTE_CONFLICT' | 'RESS_ROUTE_PATTERN_INVALID' | 'RESS_BUILD_MISSING' | 'RESS_BUILD_INCOMPATIBLE' | 'RESS_PORT_IN_USE' | 'RESS_SERVER_FILE_INVALID' | 'RESS_SERVER_FILE_THREW' | 'RESS_BAD_USAGE' | 'RESS_PLATFORM_TOKEN_INVALID';
10
+ /**
11
+ * Lo que la CLI necesita de un `PlatformTokenError` (de `@ressjs/platform`).
12
+ *
13
+ * Es estructural a propósito: la CLI no puede importar el paquete en el
14
+ * arranque —cargaría React antes de fijar `NODE_ENV`—, así que lo reconoce por
15
+ * su `code` estable.
16
+ */
17
+ export interface PlatformTokenFailure extends Error {
18
+ code: 'RESS_PLATFORM_TOKEN';
19
+ kind: 'retired' | 'unknown';
20
+ token: string;
21
+ origin?: string;
22
+ replacement?: string;
23
+ reason?: string;
24
+ suggestion?: string;
25
+ valid: readonly string[];
26
+ }
27
+ export declare function isPlatformTokenFailure(error: unknown): error is PlatformTokenFailure;
10
28
  export interface RessErrorInit {
11
29
  title: string;
12
30
  files?: string[];
@@ -27,6 +45,7 @@ export declare class RessError extends Error {
27
45
  static configInvalid(reason: string, files?: string[], cause?: unknown): RessError;
28
46
  static configNotFound(file: string): RessError;
29
47
  static routeConflict(pattern: string, files: string[]): RessError;
48
+ static platformToken(cause: PlatformTokenFailure): RessError;
30
49
  static routePatternInvalid(pattern: string, file: string, reason: string): RessError;
31
50
  static buildMissing(outDir: string): RessError;
32
51
  static buildIncompatible(found: number, expected: number): RessError;
@@ -6,6 +6,9 @@
6
6
  * excepción del código del usuario, un fallo de compilación— se presenta con su
7
7
  * traza tal cual, porque ocultarla sería peor.
8
8
  */
9
+ export function isPlatformTokenFailure(error) {
10
+ return error instanceof Error && error.code === 'RESS_PLATFORM_TOKEN';
11
+ }
9
12
  export class RessError extends Error {
10
13
  code;
11
14
  title;
@@ -53,6 +56,22 @@ export class RessError extends Error {
53
56
  exitCode: 1,
54
57
  });
55
58
  }
59
+ static platformToken(cause) {
60
+ const retired = cause.kind === 'retired';
61
+ return new RessError('RESS_PLATFORM_TOKEN_INVALID', {
62
+ title: retired ? 'Token de plataforma retirado' : 'Token de plataforma desconocido',
63
+ files: cause.origin ? [cause.origin] : undefined,
64
+ reason: retired
65
+ ? `El token "${cause.token}" ya no existe. Se retiró porque ${cause.reason}.`
66
+ : `El token "${cause.token}" no es de ningún eje de plataforma.` +
67
+ (cause.suggestion ? ` ¿Quisiste decir "${cause.suggestion}"?` : ''),
68
+ action: retired
69
+ ? `Usá ${cause.replacement}.`
70
+ : `Usá uno de los tokens válidos: ${cause.valid.join(', ')}.`,
71
+ exitCode: 1,
72
+ cause,
73
+ });
74
+ }
56
75
  static routePatternInvalid(pattern, file, reason) {
57
76
  return new RessError('RESS_ROUTE_PATTERN_INVALID', {
58
77
  title: 'Patrón de ruta inválido',
@@ -17,7 +17,7 @@ import fs from 'node:fs/promises';
17
17
  import path from 'node:path';
18
18
  import { pathToFileURL } from 'node:url';
19
19
  import { RessError } from './errors.js';
20
- const CANDIDATE_EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs'];
20
+ const CANDIDATE_EXTENSIONS = ['.ts', '.js', '.mjs', '.tsx'];
21
21
  /** Autodetección: el primero que exista, en este orden. */
22
22
  export async function findServerFile(root) {
23
23
  for (const ext of CANDIDATE_EXTENSIONS) {
@@ -9,7 +9,8 @@ export { createRessApp } from './app.js';
9
9
  export { startDevServer } from './dev.js';
10
10
  export { startProdServer } from './start.js';
11
11
  export { buildApp } from './build.js';
12
- export { inspectProject, cliVersion } from './inspect.js';
12
+ export { inspectProject } from './inspect.js';
13
+ export { cliVersion } from './version.js';
13
14
  export { resolveOptions } from './options.js';
14
15
  export { silentReporter, collectingReporter } from './reporter.js';
15
16
  export { RessError } from './errors.js';
package/dist/api/index.js CHANGED
@@ -9,7 +9,8 @@ export { createRessApp } from './app.js';
9
9
  export { startDevServer } from './dev.js';
10
10
  export { startProdServer } from './start.js';
11
11
  export { buildApp } from './build.js';
12
- export { inspectProject, cliVersion } from './inspect.js';
12
+ export { inspectProject } from './inspect.js';
13
+ export { cliVersion } from './version.js';
13
14
  export { resolveOptions } from './options.js';
14
15
  export { silentReporter, collectingReporter } from './reporter.js';
15
16
  export { RessError } from './errors.js';
@@ -63,13 +63,4 @@ export interface ProjectInfo {
63
63
  };
64
64
  };
65
65
  }
66
- /**
67
- * La versión de `@ressjs/cli`, leída de su propio `package.json`.
68
- *
69
- * Exportada porque `src/cli/main.ts` también la necesita para `--version` y no
70
- * puede tocar `node:fs` directamente sin romper la regla de separación —
71
- * "la CLI no reimplementa" incluye no leer archivos por su cuenta cuando la
72
- * API ya sabe cómo.
73
- */
74
- export declare function cliVersion(): Promise<string>;
75
66
  export declare function inspectProject(opts?: InspectOptions): Promise<ProjectInfo>;
@@ -8,10 +8,10 @@
8
8
  */
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { fileURLToPath, URL } from 'node:url';
12
11
  import { readFile } from 'node:fs/promises';
13
- import { buildVariantCatalog, getPages, loadRouteManifest, OUT_DIR, scanRoutes, } from '@ressjs/vite-router';
12
+ import { buildVariantCatalog, DEFAULT_EXTENSION_POLICY, getPages, loadRouteManifest, OUT_DIR, scanRoutes, setExtensionPolicy, writeProjectTypes, } from '@ressjs/vite-router';
14
13
  import { resolveOptions } from './options.js';
14
+ import { cliVersion } from './version.js';
15
15
  import { RessError } from './errors.js';
16
16
  async function dirBytes(dir) {
17
17
  const { readdir, stat } = await import('node:fs/promises');
@@ -39,33 +39,15 @@ async function packageVersion(root, name) {
39
39
  return undefined;
40
40
  }
41
41
  }
42
- /**
43
- * La versión de `@ressjs/cli`, leída de su propio `package.json`.
44
- *
45
- * Exportada porque `src/cli/main.ts` también la necesita para `--version` y no
46
- * puede tocar `node:fs` directamente sin romper la regla de separación —
47
- * "la CLI no reimplementa" incluye no leer archivos por su cuenta cuando la
48
- * API ya sabe cómo.
49
- */
50
- export async function cliVersion() {
51
- try {
52
- const raw = await readFile(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf-8');
53
- return JSON.parse(raw).version ?? '0.0.0';
54
- }
55
- catch {
56
- return '0.0.0';
57
- }
58
- }
59
42
  export async function inspectProject(opts = {}) {
60
43
  const root = opts.root ? path.resolve(opts.root) : process.cwd();
61
- const mode = opts.mode ?? 'development';
62
44
  let sources = {};
63
45
  let resolvedConfig = null;
64
46
  let configFile = null;
65
47
  let valid = true;
66
48
  let errors;
67
49
  try {
68
- const options = await resolveOptions({ ...opts, root, mode }, { mode });
50
+ const options = await resolveOptions({ ...opts, root }, { mode: 'development' });
69
51
  for (const [key, entry] of Object.entries(options)) {
70
52
  if (key === 'config')
71
53
  continue;
@@ -74,6 +56,13 @@ export async function inspectProject(opts = {}) {
74
56
  }
75
57
  resolvedConfig = options.config;
76
58
  configFile = options.configFile.value;
59
+ // Qué archivos son páginas lo decide `extensions`. El plugin de Vite fija esa
60
+ // política al arrancar; `ress info` no levanta Vite, así que la fija acá, o
61
+ // listaría páginas que el servidor no sirve.
62
+ setExtensionPolicy({ ...DEFAULT_EXTENSION_POLICY, ...options.config.extensions });
63
+ // Los tipos de `config.X` salen de la configuración resuelta. Generarlos acá
64
+ // permite tipar un clon limpio sin levantar el servidor (`ress info && tsc`).
65
+ await writeProjectTypes(root, options.config.platform);
77
66
  }
78
67
  catch (cause) {
79
68
  valid = false;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Flags que la CLI acepta pero todavía no tienen efecto.
3
+ *
4
+ * Aceptarlos sin decir nada es peor que no tenerlos: parece que funcionan. Cada
5
+ * comando que los recibe avisa con estos mensajes, y la documentación de la CLI
6
+ * los lista como «aceptados, sin efecto».
7
+ */
8
+ /** `--out-dir`: el pipeline escribe y lee siempre `dist/`. */
9
+ export declare function outDirIgnored(flag: string, actual: string): string;
10
+ /** `ress dev --mode`: el servidor de desarrollo siempre usa la capa de configuración `development`. */
11
+ export declare function devModeIgnored(flag: string): string;
12
+ /** Los niveles de log de Vite son cuatro; `debug` de ress se queda en `info`. */
13
+ export declare function toViteLogLevel(level: 'silent' | 'error' | 'warn' | 'info' | 'debug'): 'silent' | 'error' | 'warn' | 'info';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Flags que la CLI acepta pero todavía no tienen efecto.
3
+ *
4
+ * Aceptarlos sin decir nada es peor que no tenerlos: parece que funcionan. Cada
5
+ * comando que los recibe avisa con estos mensajes, y la documentación de la CLI
6
+ * los lista como «aceptados, sin efecto».
7
+ */
8
+ /** `--out-dir`: el pipeline escribe y lee siempre `dist/`. */
9
+ export function outDirIgnored(flag, actual) {
10
+ return `--out-dir "${flag}" se registró, pero por ahora ress siempre escribe y sirve el build desde "${actual}".`;
11
+ }
12
+ /** `ress dev --mode`: el servidor de desarrollo siempre usa la capa de configuración `development`. */
13
+ export function devModeIgnored(flag) {
14
+ return (`--mode ${flag} se registró, pero \`ress dev\` siempre usa la capa de configuración "development". ` +
15
+ 'Para probar la de producción, corré `ress build` y `ress start`.');
16
+ }
17
+ /** Los niveles de log de Vite son cuatro; `debug` de ress se queda en `info`. */
18
+ export function toViteLogLevel(level) {
19
+ return level === 'debug' ? 'info' : level;
20
+ }
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Resolución de opciones con procedencia.
3
+ *
4
+ * Precedencia fija: **argumento > variable de entorno > `config/` (F-008) >
5
+ * default del framework.** No se devuelven valores pelados: cada campo lleva su
6
+ * `source` y su `origin`, porque UC-10 pide que sea observable cuál ganó y
7
+ * `ress info` lo muestra.
8
+ *
9
+ * Variables de entorno leídas, y solo estas: `PORT`, `HOST`, `NODE_ENV`,
10
+ * `RESS_LOG_LEVEL`, `RESS_CONFIG`. `NODE_ENV` sólo se lee para derivar `mode`;
11
+ * nunca se escribe acá.
12
+ *
13
+ * Límite conocido: `loadConfig()` de F-008 resuelve `config/` a partir de
14
+ * `root` + `mode` y no acepta todavía una ruta de archivo explícita. Por eso
15
+ * `--config` / `RESS_CONFIG` quedan registrados con su procedencia pero no
16
+ * cambian qué archivo lee `loadConfig` — es una limitación de F-008, no de esta
17
+ * spec, y se deja documentada en vez de reimplementarse acá.
18
+ */
1
19
  import { type ResolvedConfig } from '@ressjs/vite-router';
2
20
  import type { LogLevel, RessAppOptions, RessMode } from './types.js';
3
21
  export type OptionSource = 'flag' | 'env' | 'config' | 'default';
@@ -16,8 +16,7 @@
16
16
  * cambian qué archivo lee `loadConfig` — es una limitación de F-008, no de esta
17
17
  * spec, y se deja documentada en vez de reimplementarse acá.
18
18
  */
19
- import path from 'node:path';
20
- import { loadConfig } from '@ressjs/vite-router';
19
+ import { canonicalPath, loadConfig, normalizeBasePath, validateConfig } from '@ressjs/vite-router';
21
20
  import { RessError } from './errors.js';
22
21
  const VALID_MODES = ['development', 'production'];
23
22
  const VALID_LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug'];
@@ -27,7 +26,7 @@ function opt(value, source, origin) {
27
26
  export async function resolveOptions(opts, defaults = {}) {
28
27
  const env = process.env;
29
28
  const root = opts.root
30
- ? opt(path.resolve(opts.root), 'flag', '--root')
29
+ ? opt(canonicalPath(opts.root), 'flag', '--root')
31
30
  : opt(process.cwd(), 'default', 'process.cwd()');
32
31
  const configFile = opts.configFile
33
32
  ? opt(opts.configFile, 'flag', '--config')
@@ -49,6 +48,11 @@ export async function resolveOptions(opts, defaults = {}) {
49
48
  const host = resolveHost(opts, defaults, env);
50
49
  const strictPort = resolveStrictPort(opts);
51
50
  const base = resolveBase(opts, config);
51
+ // El prefijo efectivo es uno solo. Quien lo quita de cada petición, el que
52
+ // prefija los artefactos y el de Vite leen `config.basePath`: si la flag lo
53
+ // cambiara sólo en `base`, la app respondería en `/` y no bajo el prefijo.
54
+ if (base.source === 'flag')
55
+ config = { ...config, basePath: base.value === '/' ? '' : base.value };
52
56
  const logLevel = resolveLogLevel(opts, env);
53
57
  const serverFile = resolveServerFile(opts);
54
58
  const outDir = opts.outDir
@@ -115,7 +119,15 @@ function resolveStrictPort(opts) {
115
119
  }
116
120
  function resolveBase(opts, config) {
117
121
  if (opts.base !== undefined) {
118
- return opt(opts.base, 'flag', '--base');
122
+ // Misma validación y misma forma canónica que `basePath` en la config:
123
+ // `--base /app/` y `basePath: '/app/'` son la misma cosa.
124
+ try {
125
+ validateConfig({ basePath: opts.base });
126
+ }
127
+ catch (cause) {
128
+ throw RessError.badUsage(`--base ${JSON.stringify(opts.base)} no es un prefijo válido: ${cause instanceof Error ? cause.message : cause}`);
129
+ }
130
+ return opt(normalizeBasePath(opts.base) || '/', 'flag', '--base');
119
131
  }
120
132
  if (config.basePath) {
121
133
  return opt(config.basePath, 'config', 'config:basePath');
@@ -0,0 +1,3 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { RessAppOptions } from './types.js';
3
+ export declare const serverEntryOptions: AsyncLocalStorage<RessAppOptions>;
@@ -0,0 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ // CLI options follow calls made by the project's entry point, including awaits.
3
+ // Nothing is stored globally or inherited by an unrelated application.
4
+ export const serverEntryOptions = new AsyncLocalStorage();
package/dist/api/start.js CHANGED
@@ -1,6 +1,12 @@
1
- import { createRessApp } from './app.js';
1
+ import { OUT_DIR, createRessApp } from './app.js';
2
+ import { outDirIgnored } from './limits.js';
3
+ import { silentReporter } from './reporter.js';
2
4
  /** `ress start`: construye la app en modo producción y la pone a escuchar. */
3
5
  export async function startProdServer(opts = {}) {
6
+ if (opts.outDir && opts.outDir !== OUT_DIR.base) {
7
+ ;
8
+ (opts.reporter ?? silentReporter).event({ type: 'warn', message: outDirIgnored(opts.outDir, OUT_DIR.base) });
9
+ }
4
10
  const app = await createRessApp({ ...opts, mode: 'production' });
5
11
  return app.listen();
6
12
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * La versión de `@ressjs/cli`, leída de su propio `package.json`.
3
+ *
4
+ * Vive en un módulo propio, sin dependencias del runtime, porque
5
+ * `src/cli/main.ts` la necesita antes de fijar `NODE_ENV`. Importarla desde
6
+ * `inspect.ts` arrastraba `@ressjs/vite-router` y, con él, React: React elige
7
+ * su build de desarrollo o de producción al cargarse, así que quedaba en
8
+ * desarrollo aunque después se fijara `NODE_ENV=production`.
9
+ */
10
+ export declare function cliVersion(): Promise<string>;
@@ -0,0 +1,20 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath, URL } from 'node:url';
3
+ /**
4
+ * La versión de `@ressjs/cli`, leída de su propio `package.json`.
5
+ *
6
+ * Vive en un módulo propio, sin dependencias del runtime, porque
7
+ * `src/cli/main.ts` la necesita antes de fijar `NODE_ENV`. Importarla desde
8
+ * `inspect.ts` arrastraba `@ressjs/vite-router` y, con él, React: React elige
9
+ * su build de desarrollo o de producción al cargarse, así que quedaba en
10
+ * desarrollo aunque después se fijara `NODE_ENV=production`.
11
+ */
12
+ export async function cliVersion() {
13
+ try {
14
+ const raw = await readFile(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf-8');
15
+ return JSON.parse(raw).version ?? '0.0.0';
16
+ }
17
+ catch {
18
+ return '0.0.0';
19
+ }
20
+ }
package/dist/cli/argv.js CHANGED
@@ -63,7 +63,7 @@ export function parseArgv(argv, version) {
63
63
  .option('--root <dir>', 'Raíz del proyecto')
64
64
  .addOption(new Option('-c, --config <file>', 'Archivo de configuración'))
65
65
  .addOption(new Option('-m, --mode <mode>', 'Modo de configuración').choices(['development', 'production']))
66
- .option('--server <file>', 'Archivo de servidor del proyecto ("false" para desactivar)')
66
+ .option('--server <file>', 'Entrada del servidor del proyecto ("false" para desactivar)')
67
67
  .addOption(new Option('-l, --log-level <lvl>', 'Detalle de la salida').choices(LOG_LEVELS))
68
68
  .option('-o, --open', 'Abre el navegador al arrancar')
69
69
  .option('--clear-screen', 'Limpia la pantalla en cada recarga')
@@ -91,7 +91,7 @@ export function parseArgv(argv, version) {
91
91
  .option('--root <dir>', 'Raíz del proyecto')
92
92
  .addOption(new Option('-c, --config <file>', 'Archivo de configuración'))
93
93
  .option('--out-dir <dir>', 'Build a servir')
94
- .option('--server <file>', 'Archivo de servidor del proyecto ("false" para desactivar)')
94
+ .option('--server <file>', 'Entrada del servidor del proyecto ("false" para desactivar)')
95
95
  .addOption(new Option('-l, --log-level <lvl>', 'Detalle de la salida').choices(LOG_LEVELS))
96
96
  .action((opts) => {
97
97
  result = { command: 'start', flags: { ...opts, server: normalizeServer(opts.server) } };
@@ -1,7 +1,12 @@
1
+ import path from 'node:path';
1
2
  import { buildApp } from '../../api/index.js';
2
3
  import { createTerminalReporter } from '../reporters/terminal.js';
3
4
  export async function buildCommand(flags) {
5
+ // Page/style discovery uses cwd as well as Vite's root. Normalize before
6
+ // changing cwd so a relative --root is not resolved a second time.
7
+ const root = path.resolve(flags.root ?? process.cwd());
8
+ process.chdir(root);
4
9
  const reporter = createTerminalReporter({ logLevel: flags.logLevel });
5
- await buildApp({ ...flags, reporter });
10
+ await buildApp({ ...flags, root, reporter });
6
11
  return 0;
7
12
  }
@@ -1,10 +1,14 @@
1
- import { startDevServer } from '../../api/index.js';
1
+ import { runServerEntry } from '../server-entry.js';
2
2
  import { createTerminalReporter } from '../reporters/terminal.js';
3
3
  import { registerShutdown } from '../shutdown.js';
4
4
  export async function devCommand(flags) {
5
+ const entry = await runServerEntry(flags, flags.mode ?? 'development');
6
+ if (entry.handled)
7
+ return 0;
8
+ const { startDevServer } = await import('../../api/index.js');
5
9
  const { server: serverFile, ...rest } = flags;
6
10
  const reporter = createTerminalReporter({ logLevel: flags.logLevel });
7
- const server = await startDevServer({ ...rest, serverFile, reporter });
11
+ const server = await startDevServer({ ...rest, root: entry.root, serverFile: entry.extension ?? serverFile, reporter });
8
12
  registerShutdown(() => server.close());
9
13
  return new Promise(() => { }); // dev no retorna hasta la señal
10
14
  }
@@ -1,10 +1,14 @@
1
- import { startProdServer } from '../../api/index.js';
1
+ import { runServerEntry } from '../server-entry.js';
2
2
  import { createTerminalReporter } from '../reporters/terminal.js';
3
3
  import { registerShutdown } from '../shutdown.js';
4
4
  export async function startCommand(flags) {
5
+ const entry = await runServerEntry(flags, 'production');
6
+ if (entry.handled)
7
+ return 0;
8
+ const { startProdServer } = await import('../../api/index.js');
5
9
  const { server: serverFile, ...rest } = flags;
6
10
  const reporter = createTerminalReporter({ logLevel: flags.logLevel });
7
- const server = await startProdServer({ ...rest, serverFile, reporter });
11
+ const server = await startProdServer({ ...rest, root: entry.root, serverFile: entry.extension ?? serverFile, reporter });
8
12
  registerShutdown(() => server.close());
9
13
  return new Promise(() => { }); // start no retorna hasta la señal
10
14
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `NODE_ENV` por comando.
3
+ *
4
+ * `ress start` y `ress build` no fijaban `NODE_ENV`, así que en producción
5
+ * React (y cualquier caché que lea `process.env.NODE_ENV`) seguía corriendo
6
+ * en modo desarrollo salvo que quien desplegara lo supiera y lo pusiera a
7
+ * mano. Se fija acá, antes de importar el runtime (`../../api/index.js`,
8
+ * detrás de un `import()` dinámico en `main.ts`), y no lo pisa si ya venía
9
+ * declarado desde afuera.
10
+ */
11
+ export type EnvCommand = 'dev' | 'build' | 'start' | 'info' | 'help' | 'version';
12
+ export declare function applyNodeEnvDefault(command: EnvCommand): void;
@@ -0,0 +1,9 @@
1
+ export function applyNodeEnvDefault(command) {
2
+ if (command === 'dev') {
3
+ process.env.NODE_ENV ??= 'development';
4
+ return;
5
+ }
6
+ if (command === 'build' || command === 'start') {
7
+ process.env.NODE_ENV ??= 'production';
8
+ }
9
+ }
package/dist/cli/main.js CHANGED
@@ -6,24 +6,27 @@
6
6
  * en `commands/*`.
7
7
  */
8
8
  import { parseArgv } from './argv.js';
9
- import { devCommand } from './commands/dev.js';
10
- import { buildCommand } from './commands/build.js';
11
- import { startCommand } from './commands/start.js';
12
- import { infoCommand } from './commands/info.js';
13
9
  import { formatError } from './reporters/terminal.js';
14
10
  import { detectFormatEnv } from './reporters/format.js';
15
- import { RessError } from '../api/errors.js';
16
- import { cliVersion } from '../api/inspect.js';
17
- function dispatch(parsed) {
11
+ import { applyNodeEnvDefault } from './env.js';
12
+ import { RessError, isPlatformTokenFailure } from '../api/errors.js';
13
+ // Sólo módulos sin el runtime: `cliVersion` vive aparte de `inspect.ts`, que
14
+ // importa `@ressjs/vite-router` y con él React, y React tiene que cargarse
15
+ // después de `applyNodeEnvDefault`.
16
+ import { cliVersion } from '../api/version.js';
17
+ // Import dinámico y no estático: así `NODE_ENV` queda fijado (`applyNodeEnvDefault`,
18
+ // llamado antes de esto) antes de que se cargue `../../api/index.js`, que es
19
+ // donde vive el runtime que lee `process.env.NODE_ENV`.
20
+ async function dispatch(parsed) {
18
21
  switch (parsed.command) {
19
22
  case 'dev':
20
- return devCommand(parsed.flags);
23
+ return (await import('./commands/dev.js')).devCommand(parsed.flags);
21
24
  case 'build':
22
- return buildCommand(parsed.flags);
25
+ return (await import('./commands/build.js')).buildCommand(parsed.flags);
23
26
  case 'start':
24
- return startCommand(parsed.flags);
27
+ return (await import('./commands/start.js')).startCommand(parsed.flags);
25
28
  case 'info':
26
- return infoCommand(parsed.flags);
29
+ return (await import('./commands/info.js')).infoCommand(parsed.flags);
27
30
  }
28
31
  }
29
32
  export async function main(argv = process.argv.slice(2)) {
@@ -41,13 +44,17 @@ export async function main(argv = process.argv.slice(2)) {
41
44
  process.exitCode = 0;
42
45
  return;
43
46
  }
47
+ applyNodeEnvDefault(parsed.command);
44
48
  process.exitCode = await dispatch(parsed);
45
49
  }
46
50
  catch (error) {
47
51
  const env = detectFormatEnv(Boolean(process.stderr.isTTY));
48
- if (error instanceof RessError) {
49
- process.stderr.write(formatError(error, env) + '\n');
50
- process.exitCode = error.exitCode;
52
+ // Un token de plataforma inválido lo lanza `@ressjs/platform` desde un hook
53
+ // de Vite: se presenta con el formato de los demás errores del proyecto.
54
+ const known = isPlatformTokenFailure(error) ? RessError.platformToken(error) : error;
55
+ if (known instanceof RessError) {
56
+ process.stderr.write(formatError(known, env) + '\n');
57
+ process.exitCode = known.exitCode;
51
58
  return;
52
59
  }
53
60
  // No identificado por el framework: se muestra la traza tal cual. Ocultarla
@@ -0,0 +1,7 @@
1
+ import type { DevFlags, StartFlags } from './argv.js';
2
+ /** Run the project's server before constructing the framework's default app. */
3
+ export declare function runServerEntry(flags: DevFlags | StartFlags, mode: 'development' | 'production'): Promise<{
4
+ handled: boolean;
5
+ root: string;
6
+ extension?: string;
7
+ }>;
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { findServerFile } from '../api/extend.js';
4
+ import { serverEntryOptions } from '../api/server-context.js';
5
+ import { RessError } from '../api/errors.js';
6
+ /** Run the project's server before constructing the framework's default app. */
7
+ export async function runServerEntry(flags, mode) {
8
+ const root = path.resolve(flags.root ?? process.cwd());
9
+ // Vite and route discovery also use cwd. Apply --root for the default path,
10
+ // including --server false, rather than only for a project-owned entry.
11
+ process.chdir(root);
12
+ if (flags.server === false)
13
+ return { handled: false, root };
14
+ const file = flags.server ? path.resolve(root, flags.server) : await findServerFile(root);
15
+ if (!file)
16
+ return { handled: false, root };
17
+ // JSX server extensions still use the framework's existing module loader.
18
+ if (path.extname(file) === '.tsx')
19
+ return { handled: false, root, extension: file };
20
+ if (path.extname(file) === '.ts' && !process.features.typescript) {
21
+ throw RessError.badUsage('Para ejecutar server.ts se necesita Node 22.18+ con soporte TypeScript habilitado.');
22
+ }
23
+ const { server: _server, config, ...rest } = flags;
24
+ const options = {
25
+ ...rest, root, configFile: config, mode, serverFile: false,
26
+ };
27
+ process.env.NODE_ENV = mode;
28
+ let mod;
29
+ try {
30
+ mod = await serverEntryOptions.run(options, () => import(pathToFileURL(file).href));
31
+ }
32
+ catch (cause) {
33
+ if (cause instanceof RessError)
34
+ throw cause;
35
+ throw RessError.serverFileThrew(file, cause);
36
+ }
37
+ // Preserve the existing default-export setup callback contract.
38
+ const exported = mod.default;
39
+ if (typeof mod.default === 'function' && typeof exported?.listen !== 'function') {
40
+ return { handled: false, root, extension: file };
41
+ }
42
+ return { handled: true, root };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ressjs/cli",
3
- "version": "0.6.0-experimental.0",
3
+ "version": "0.6.0-rc.1",
4
4
  "description": "La CLI de ress.js: dev, build, start e info sobre la misma construcción de la aplicación que expone como API.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,9 +37,9 @@
37
37
  "vite"
38
38
  ],
39
39
  "author": "ress.js team",
40
- "license": "MIT",
40
+ "license": "SEE LICENSE IN LICENSE",
41
41
  "dependencies": {
42
- "@ressjs/vite-router": "0.6.0-experimental.0",
42
+ "@ressjs/vite-router": "0.6.0-rc.1",
43
43
  "@vitejs/plugin-react": "^6.1.0",
44
44
  "commander": "^12.1.0",
45
45
  "express": "^5.1.0"