@ressjs/cli 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.
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,7 @@
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
11
  import { readFile } from 'node:fs/promises';
12
- import { buildVariantCatalog, getPages, loadRouteManifest, OUT_DIR, scanRoutes, writeProjectTypes, } from '@ressjs/vite-router';
12
+ import { buildVariantCatalog, DEFAULT_EXTENSION_POLICY, getPages, loadRouteManifest, OUT_DIR, scanRoutes, setExtensionPolicy, writeProjectTypes, } from '@ressjs/vite-router';
13
13
  import { resolveOptions } from './options.js';
14
14
  import { cliVersion } from './version.js';
15
15
  import { RessError } from './errors.js';
@@ -41,14 +41,13 @@ async function packageVersion(root, name) {
41
41
  }
42
42
  export async function inspectProject(opts = {}) {
43
43
  const root = opts.root ? path.resolve(opts.root) : process.cwd();
44
- const mode = opts.mode ?? 'development';
45
44
  let sources = {};
46
45
  let resolvedConfig = null;
47
46
  let configFile = null;
48
47
  let valid = true;
49
48
  let errors;
50
49
  try {
51
- const options = await resolveOptions({ ...opts, root, mode }, { mode });
50
+ const options = await resolveOptions({ ...opts, root }, { mode: 'development' });
52
51
  for (const [key, entry] of Object.entries(options)) {
53
52
  if (key === 'config')
54
53
  continue;
@@ -57,6 +56,10 @@ export async function inspectProject(opts = {}) {
57
56
  }
58
57
  resolvedConfig = options.config;
59
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 });
60
63
  // Los tipos de `config.X` salen de la configuración resuelta. Generarlos acá
61
64
  // permite tipar un clon limpio sin levantar el servidor (`ress info && tsc`).
62
65
  await writeProjectTypes(root, options.config.platform);
@@ -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
  }
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
  }
package/dist/cli/main.js CHANGED
@@ -9,7 +9,7 @@ import { parseArgv } from './argv.js';
9
9
  import { formatError } from './reporters/terminal.js';
10
10
  import { detectFormatEnv } from './reporters/format.js';
11
11
  import { applyNodeEnvDefault } from './env.js';
12
- import { RessError } from '../api/errors.js';
12
+ import { RessError, isPlatformTokenFailure } from '../api/errors.js';
13
13
  // Sólo módulos sin el runtime: `cliVersion` vive aparte de `inspect.ts`, que
14
14
  // importa `@ressjs/vite-router` y con él React, y React tiene que cargarse
15
15
  // después de `applyNodeEnvDefault`.
@@ -49,9 +49,12 @@ export async function main(argv = process.argv.slice(2)) {
49
49
  }
50
50
  catch (error) {
51
51
  const env = detectFormatEnv(Boolean(process.stderr.isTTY));
52
- if (error instanceof RessError) {
53
- process.stderr.write(formatError(error, env) + '\n');
54
- 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;
55
58
  return;
56
59
  }
57
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-rc.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": {
@@ -39,7 +39,7 @@
39
39
  "author": "ress.js team",
40
40
  "license": "SEE LICENSE IN LICENSE",
41
41
  "dependencies": {
42
- "@ressjs/vite-router": "0.6.0-rc.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"