@ressjs/cli 0.6.0-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ress.js +2 -0
- package/dist/api/app.d.ts +4 -0
- package/dist/api/app.js +189 -0
- package/dist/api/build.d.ts +8 -0
- package/dist/api/build.js +113 -0
- package/dist/api/dev.d.ts +3 -0
- package/dist/api/dev.js +6 -0
- package/dist/api/errors.d.ts +37 -0
- package/dist/api/errors.js +119 -0
- package/dist/api/extend.d.ts +29 -0
- package/dist/api/extend.js +117 -0
- package/dist/api/index.d.ts +22 -0
- package/dist/api/index.js +16 -0
- package/dist/api/inspect.d.ts +75 -0
- package/dist/api/inspect.js +170 -0
- package/dist/api/options.d.ts +29 -0
- package/dist/api/options.js +152 -0
- package/dist/api/reporter.d.ts +74 -0
- package/dist/api/reporter.js +12 -0
- package/dist/api/start.d.ts +3 -0
- package/dist/api/start.js +6 -0
- package/dist/api/types.d.ts +82 -0
- package/dist/api/types.js +1 -0
- package/dist/cli/argv.d.ts +63 -0
- package/dist/cli/argv.js +148 -0
- package/dist/cli/commands/build.d.ts +2 -0
- package/dist/cli/commands/build.js +7 -0
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +10 -0
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +17 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.js +10 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +62 -0
- package/dist/cli/reporters/format.d.ts +24 -0
- package/dist/cli/reporters/format.js +50 -0
- package/dist/cli/reporters/info-format.d.ts +15 -0
- package/dist/cli/reporters/info-format.js +49 -0
- package/dist/cli/reporters/json.d.ts +12 -0
- package/dist/cli/reporters/json.js +13 -0
- package/dist/cli/reporters/terminal.d.ts +35 -0
- package/dist/cli/reporters/terminal.js +118 -0
- package/dist/cli/shutdown.d.ts +2 -0
- package/dist/cli/shutdown.js +17 -0
- package/package.json +59 -0
package/bin/ress.js
ADDED
package/dist/api/app.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createRessApp` — la única construcción de la aplicación de servidor.
|
|
3
|
+
*
|
|
4
|
+
* `startDevServer`, `startProdServer`, el archivo de servidor del proyecto (vía
|
|
5
|
+
* de escape nivel 2) y el uso programático (nivel 3) comparten esta función. No
|
|
6
|
+
* hay una cuarta forma de construir la app.
|
|
7
|
+
*
|
|
8
|
+
* Reutiliza lo que F-015/F-018 ya construyen — `createRuntime` arma el servidor
|
|
9
|
+
* de Vite o los artefactos de producción, `createViteRouter` monta el
|
|
10
|
+
* enrutador— en vez de reimplementarlos. El plan de esta spec nombra un
|
|
11
|
+
* `createRessRouter` que no existe como tal en `@ressjs/vite-router`: lo que
|
|
12
|
+
* el paquete expone es `createViteRouter`, que hace exactamente lo mismo
|
|
13
|
+
* (enumera rutas, construye el pipeline de petición, se mantiene al día con un
|
|
14
|
+
* watcher). Se usa ese nombre real en vez de inventar un alias.
|
|
15
|
+
*/
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import react from '@vitejs/plugin-react';
|
|
21
|
+
import { assetPrefixFor, createRouteManifest, createRuntime, createViteRouter, loadRouteManifest, OUT_DIR, ressPlugin, ROUTE_MANIFEST_VERSION, scanRoutes, waitForViteRouter, } from '@ressjs/vite-router';
|
|
22
|
+
import { resolveOptions } from './options.js';
|
|
23
|
+
import { silentReporter } from './reporter.js';
|
|
24
|
+
import { RessError } from './errors.js';
|
|
25
|
+
import { applyServerFile } from './extend.js';
|
|
26
|
+
/** Cierra un `http.Server`, tolerando que ya esté cerrado. Idempotente. */
|
|
27
|
+
function closeHttpServer(server) {
|
|
28
|
+
if (!server.listening)
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
server.close((err) => (err && err.code !== 'ERR_SERVER_NOT_RUNNING' ? reject(err) : resolve()));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function assertBuildPresent(root, outDir) {
|
|
35
|
+
const manifestPath = path.join(root, outDir, 'route-manifest.json');
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = await fs.readFile(manifestPath, 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw RessError.buildMissing(path.join(outDir));
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
loadRouteManifest(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
const data = JSON.parse(raw);
|
|
48
|
+
throw RessError.buildIncompatible(data.version ?? -1, ROUTE_MANIFEST_VERSION);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function createRessApp(opts = {}) {
|
|
52
|
+
const configStart = Date.now();
|
|
53
|
+
const reporter = opts.reporter ?? silentReporter;
|
|
54
|
+
// El host por defecto difiere por modo: en desarrollo, sólo esta máquina; en
|
|
55
|
+
// producción, abierto — es la tabla de `ress dev` / `ress start` del plan.
|
|
56
|
+
const mode = opts.mode ?? 'development';
|
|
57
|
+
const options = await resolveOptions(opts, {
|
|
58
|
+
mode,
|
|
59
|
+
host: mode === 'production' ? '0.0.0.0' : 'localhost',
|
|
60
|
+
});
|
|
61
|
+
reporter.event({
|
|
62
|
+
type: 'config:loaded',
|
|
63
|
+
file: options.configFile.value,
|
|
64
|
+
durationMs: Date.now() - configStart,
|
|
65
|
+
});
|
|
66
|
+
const isProduction = options.mode.value === 'production';
|
|
67
|
+
const root = options.root.value;
|
|
68
|
+
if (isProduction) {
|
|
69
|
+
await assertBuildPresent(root, options.outDir.value);
|
|
70
|
+
}
|
|
71
|
+
const app = express();
|
|
72
|
+
// Vía de escape nivel 1: antes del enrutador del framework, gana ante sus rutas.
|
|
73
|
+
if (options.config.server.beforeRouter) {
|
|
74
|
+
await options.config.server.beforeRouter(app);
|
|
75
|
+
reporter.event({ type: 'server:extended', file: 'config', hook: 'before' });
|
|
76
|
+
}
|
|
77
|
+
const assetPrefix = assetPrefixFor({
|
|
78
|
+
basePath: options.base.value,
|
|
79
|
+
cdnPrefix: options.config.assets?.cdnPrefix,
|
|
80
|
+
});
|
|
81
|
+
// Un proyecto de ress.js no tiene `vite.config.ts`: la CLI es quien le pasa a
|
|
82
|
+
// Vite el plugin del framework y el de React. Sin esto, `createDevRuntime`
|
|
83
|
+
// dependería de que Vite encontrara un archivo de configuración que, por
|
|
84
|
+
// diseño, ya no existe en el proyecto (UC-1, UC-6).
|
|
85
|
+
const runtime = await createRuntime(isProduction, options.base.value, assetPrefix, {
|
|
86
|
+
plugins: [react(), ressPlugin()],
|
|
87
|
+
});
|
|
88
|
+
const router = createViteRouter({
|
|
89
|
+
isProduction,
|
|
90
|
+
basePath: options.base.value,
|
|
91
|
+
config: options.config,
|
|
92
|
+
runtime,
|
|
93
|
+
});
|
|
94
|
+
// `listen()` sólo queda disponible después de que el runtime, el manifest y
|
|
95
|
+
// las cadenas de rutas terminaron de construirse. Así no existe una ventana
|
|
96
|
+
// en la que el socket acepta conexiones pero las páginas todavía no están.
|
|
97
|
+
await waitForViteRouter(router);
|
|
98
|
+
app.use('/', router);
|
|
99
|
+
// Vía de escape nivel 1, segunda mitad: después del enrutador, no puede pisarlo.
|
|
100
|
+
if (options.config.server.afterRouter) {
|
|
101
|
+
await options.config.server.afterRouter(app);
|
|
102
|
+
reporter.event({ type: 'server:extended', file: 'config', hook: 'after' });
|
|
103
|
+
}
|
|
104
|
+
const routesStart = Date.now();
|
|
105
|
+
const manifest = isProduction
|
|
106
|
+
? runtime.routes
|
|
107
|
+
: createRouteManifest(await scanRoutes({ pagesDir: path.join(root, 'app/pages') }));
|
|
108
|
+
reporter.event({
|
|
109
|
+
type: 'routes:scanned',
|
|
110
|
+
entries: [...manifest.entries],
|
|
111
|
+
durationMs: Date.now() - routesStart,
|
|
112
|
+
});
|
|
113
|
+
reporter.event({
|
|
114
|
+
type: 'catalog:built',
|
|
115
|
+
pages: runtime.catalog.pagesById.size,
|
|
116
|
+
variants: [...new Set(runtime.catalog.buildEntries.map((e) => e.variant.suffix).filter(Boolean))],
|
|
117
|
+
});
|
|
118
|
+
// Vía de escape nivel 2: el archivo de servidor del proyecto.
|
|
119
|
+
await applyServerFile({
|
|
120
|
+
app,
|
|
121
|
+
options,
|
|
122
|
+
mode: options.mode.value,
|
|
123
|
+
manifest,
|
|
124
|
+
vite: runtime.vite ?? undefined,
|
|
125
|
+
reporter,
|
|
126
|
+
moduleLoader: isProduction ? undefined : runtime.moduleLoader,
|
|
127
|
+
onSourceChange: isProduction ? undefined : runtime.onSourceChange,
|
|
128
|
+
});
|
|
129
|
+
let httpServer;
|
|
130
|
+
let closed = false;
|
|
131
|
+
const bindPort = (server, host, port, attempt = 0) => new Promise((resolve, reject) => {
|
|
132
|
+
const onError = (err) => {
|
|
133
|
+
server.off('listening', onListening);
|
|
134
|
+
if (err.code === 'EADDRINUSE') {
|
|
135
|
+
if (options.strictPort.value || attempt >= 20) {
|
|
136
|
+
return reject(RessError.portInUse(port));
|
|
137
|
+
}
|
|
138
|
+
return resolve(bindPort(server, host, port + 1, attempt + 1));
|
|
139
|
+
}
|
|
140
|
+
reject(err);
|
|
141
|
+
};
|
|
142
|
+
const onListening = () => {
|
|
143
|
+
server.off('error', onError);
|
|
144
|
+
// `port: 0` le pide al SO un puerto libre; el que realmente se usó
|
|
145
|
+
// sólo se sabe después de escuchar, leyendo la dirección real.
|
|
146
|
+
const address = server.address();
|
|
147
|
+
resolve(typeof address === 'object' && address ? address.port : port);
|
|
148
|
+
};
|
|
149
|
+
server.once('error', onError);
|
|
150
|
+
server.once('listening', onListening);
|
|
151
|
+
server.listen(port, host);
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
app,
|
|
155
|
+
options,
|
|
156
|
+
manifest,
|
|
157
|
+
vite: runtime.vite ?? undefined,
|
|
158
|
+
async listen() {
|
|
159
|
+
if (httpServer)
|
|
160
|
+
throw new Error('[ress] Este RessApp ya está escuchando.');
|
|
161
|
+
const server = http.createServer(app);
|
|
162
|
+
httpServer = server;
|
|
163
|
+
const host = options.host.value;
|
|
164
|
+
const boundPort = await bindPort(server, host, options.port.value);
|
|
165
|
+
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
166
|
+
const url = `http://${displayHost}:${boundPort}${options.base.value}`;
|
|
167
|
+
reporter.event({ type: 'server:listening', url, mode: options.mode.value, port: boundPort });
|
|
168
|
+
return {
|
|
169
|
+
url,
|
|
170
|
+
port: boundPort,
|
|
171
|
+
httpServer: server,
|
|
172
|
+
// No es sólo `RessServer.close()`: si alguien la llama y después llama
|
|
173
|
+
// también a `RessApp.close()` (o al revés — el shutdown de la CLI hace
|
|
174
|
+
// exactamente eso indirectamente), el segundo cierre no puede fallar
|
|
175
|
+
// con `ERR_SERVER_NOT_RUNNING` por encontrar el socket ya cerrado.
|
|
176
|
+
close: () => closeHttpServer(server),
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
async close() {
|
|
180
|
+
if (closed)
|
|
181
|
+
return;
|
|
182
|
+
closed = true;
|
|
183
|
+
if (httpServer)
|
|
184
|
+
await closeHttpServer(httpServer);
|
|
185
|
+
await runtime.close();
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export { OUT_DIR };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuildResult, RessAppOptions } from './types.js';
|
|
2
|
+
export interface BuildAppOptions extends RessAppOptions {
|
|
3
|
+
/** Qué entorno construir. Default `'all'`: cliente y servidor, en ese orden. */
|
|
4
|
+
target?: 'all' | 'client' | 'ssr';
|
|
5
|
+
/** `false` conserva la salida anterior. Default `true`. */
|
|
6
|
+
clean?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildApp(opts?: BuildAppOptions): Promise<BuildResult>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ress build` — cliente y servidor en una sola invocación, en el orden que
|
|
3
|
+
* fija el framework.
|
|
4
|
+
*
|
|
5
|
+
* El orden lo decide `builder.buildApp` de F-015 (`defineEnvironments()` en
|
|
6
|
+
* `@ressjs/vite-router`, aplicado por `ressPlugin()`): primero cliente, después
|
|
7
|
+
* servidor, porque el servidor necesita el manifest de assets que produce el
|
|
8
|
+
* cliente. Esta función no reimplementa ese orden: crea el `builder` de Vite y
|
|
9
|
+
* lo invoca.
|
|
10
|
+
*
|
|
11
|
+
* **Límite conocido, documentado y no resuelto por esta spec**: `--out-dir` se
|
|
12
|
+
* acepta y queda registrado en `ResolvedRessOptions`, pero F-015/F-016/F-018
|
|
13
|
+
* escriben y leen siempre bajo la constante `OUT_DIR` (`dist/client`,
|
|
14
|
+
* `dist/server`, `dist/route-manifest.json`), sin parametrizarla. Cambiar eso
|
|
15
|
+
* cruza tres specs ajenas para una bandera de conveniencia; se avisa con un
|
|
16
|
+
* evento `warn` en vez de fingir que el build respeta la ruta pedida.
|
|
17
|
+
*/
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
20
|
+
import react from '@vitejs/plugin-react';
|
|
21
|
+
import { getPages, buildVariantCatalog, OUT_DIR, ressPlugin, scanRoutes } from '@ressjs/vite-router';
|
|
22
|
+
import { resolveOptions } from './options.js';
|
|
23
|
+
import { silentReporter } from './reporter.js';
|
|
24
|
+
import { RessError } from './errors.js';
|
|
25
|
+
const TARGET_DIRS = {
|
|
26
|
+
client: OUT_DIR.client,
|
|
27
|
+
ssr: OUT_DIR.ssr,
|
|
28
|
+
};
|
|
29
|
+
export async function buildApp(opts = {}) {
|
|
30
|
+
const reporter = opts.reporter ?? silentReporter;
|
|
31
|
+
const options = await resolveOptions(opts, { mode: 'production' });
|
|
32
|
+
const root = options.root.value;
|
|
33
|
+
const target = opts.target ?? 'all';
|
|
34
|
+
const clean = opts.clean ?? true;
|
|
35
|
+
if (opts.outDir && opts.outDir !== OUT_DIR.base) {
|
|
36
|
+
reporter.event({
|
|
37
|
+
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.`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Igual que el resto de los comandos: abortar antes de tocar el disco si hay
|
|
43
|
+
// un conflicto de rutas, en vez de dejar un build parcial publicable.
|
|
44
|
+
try {
|
|
45
|
+
await scanRoutes({ strict: true, pagesDir: path.join(root, 'app/pages') });
|
|
46
|
+
}
|
|
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);
|
|
51
|
+
}
|
|
52
|
+
if (clean) {
|
|
53
|
+
await rm(path.join(root, OUT_DIR.base), { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
const { createBuilder } = await import('vite');
|
|
56
|
+
const builder = await createBuilder({
|
|
57
|
+
root,
|
|
58
|
+
mode: 'production',
|
|
59
|
+
plugins: [react(), ressPlugin({ extensions: options.config.extensions })],
|
|
60
|
+
build: { emptyOutDir: false },
|
|
61
|
+
});
|
|
62
|
+
const targets = [];
|
|
63
|
+
const t0 = Date.now();
|
|
64
|
+
const buildOne = async (name) => {
|
|
65
|
+
const env = builder.environments[name];
|
|
66
|
+
if (!env) {
|
|
67
|
+
throw new Error(`[ress] El entorno "${name}" no está declarado en la configuración de Vite.`);
|
|
68
|
+
}
|
|
69
|
+
reporter.event({ type: 'build:start', target: name });
|
|
70
|
+
const tStart = Date.now();
|
|
71
|
+
await builder.build(env);
|
|
72
|
+
const durationMs = Date.now() - tStart;
|
|
73
|
+
const dir = path.join(root, TARGET_DIRS[name]);
|
|
74
|
+
const { files, bytes } = await dirStats(dir);
|
|
75
|
+
for (const file of files)
|
|
76
|
+
reporter.event({ type: 'build:artifact', file, bytes: 0 });
|
|
77
|
+
reporter.event({ type: 'build:done', target: name, durationMs, bytes });
|
|
78
|
+
targets.push({ target: name, durationMs, bytes, files });
|
|
79
|
+
};
|
|
80
|
+
if (target === 'all' || target === 'client')
|
|
81
|
+
await buildOne('client');
|
|
82
|
+
if (target === 'all' || target === 'ssr')
|
|
83
|
+
await buildOne('ssr');
|
|
84
|
+
const pages = getPages();
|
|
85
|
+
const catalog = await buildVariantCatalog({ pages });
|
|
86
|
+
return {
|
|
87
|
+
targets,
|
|
88
|
+
durationMs: Date.now() - t0,
|
|
89
|
+
outDir: OUT_DIR.base,
|
|
90
|
+
routes: pages.length,
|
|
91
|
+
variants: catalog.buildEntries.length,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function dirStats(dir) {
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = await readdir(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { files: [], bytes: 0 };
|
|
101
|
+
}
|
|
102
|
+
const files = [];
|
|
103
|
+
let bytes = 0;
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const full = path.join(dir, entry);
|
|
106
|
+
const info = await stat(full).catch(() => null);
|
|
107
|
+
if (!info || !info.isFile())
|
|
108
|
+
continue;
|
|
109
|
+
files.push(entry.split(path.sep).join('/'));
|
|
110
|
+
bytes += info.size;
|
|
111
|
+
}
|
|
112
|
+
return { files, bytes };
|
|
113
|
+
}
|
package/dist/api/dev.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errores identificados del framework.
|
|
3
|
+
*
|
|
4
|
+
* "Identificado" es la palabra clave: un `RessError` siempre tiene causa,
|
|
5
|
+
* ubicación (cuando aplica) y una acción sugerida. Lo que no pasa por acá —una
|
|
6
|
+
* excepción del código del usuario, un fallo de compilación— se presenta con su
|
|
7
|
+
* traza tal cual, porque ocultarla sería peor.
|
|
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';
|
|
10
|
+
export interface RessErrorInit {
|
|
11
|
+
title: string;
|
|
12
|
+
files?: string[];
|
|
13
|
+
reason?: string;
|
|
14
|
+
action?: string;
|
|
15
|
+
exitCode: 1 | 2 | 3;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export declare class RessError extends Error {
|
|
19
|
+
readonly code: RessErrorCode;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly files?: string[];
|
|
22
|
+
readonly reason?: string;
|
|
23
|
+
readonly action?: string;
|
|
24
|
+
readonly exitCode: 1 | 2 | 3;
|
|
25
|
+
readonly cause?: unknown;
|
|
26
|
+
constructor(code: RessErrorCode, init: RessErrorInit);
|
|
27
|
+
static configInvalid(reason: string, files?: string[], cause?: unknown): RessError;
|
|
28
|
+
static configNotFound(file: string): RessError;
|
|
29
|
+
static routeConflict(pattern: string, files: string[]): RessError;
|
|
30
|
+
static routePatternInvalid(pattern: string, file: string, reason: string): RessError;
|
|
31
|
+
static buildMissing(outDir: string): RessError;
|
|
32
|
+
static buildIncompatible(found: number, expected: number): RessError;
|
|
33
|
+
static portInUse(port: number, process_?: string): RessError;
|
|
34
|
+
static serverFileInvalid(file: string): RessError;
|
|
35
|
+
static serverFileThrew(file: string, cause: unknown): RessError;
|
|
36
|
+
static badUsage(reason: string): RessError;
|
|
37
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errores identificados del framework.
|
|
3
|
+
*
|
|
4
|
+
* "Identificado" es la palabra clave: un `RessError` siempre tiene causa,
|
|
5
|
+
* ubicación (cuando aplica) y una acción sugerida. Lo que no pasa por acá —una
|
|
6
|
+
* excepción del código del usuario, un fallo de compilación— se presenta con su
|
|
7
|
+
* traza tal cual, porque ocultarla sería peor.
|
|
8
|
+
*/
|
|
9
|
+
export class RessError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
title;
|
|
12
|
+
files;
|
|
13
|
+
reason;
|
|
14
|
+
action;
|
|
15
|
+
exitCode;
|
|
16
|
+
cause;
|
|
17
|
+
constructor(code, init) {
|
|
18
|
+
super(init.title);
|
|
19
|
+
this.name = 'RessError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.title = init.title;
|
|
22
|
+
this.files = init.files;
|
|
23
|
+
this.reason = init.reason;
|
|
24
|
+
this.action = init.action;
|
|
25
|
+
this.exitCode = init.exitCode;
|
|
26
|
+
this.cause = init.cause;
|
|
27
|
+
}
|
|
28
|
+
static configInvalid(reason, files, cause) {
|
|
29
|
+
return new RessError('RESS_CONFIG_INVALID', {
|
|
30
|
+
title: 'Configuración inválida',
|
|
31
|
+
reason,
|
|
32
|
+
files,
|
|
33
|
+
action: 'Corregí la configuración en config/ y volvé a intentar.',
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
cause,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
static configNotFound(file) {
|
|
39
|
+
return new RessError('RESS_CONFIG_NOT_FOUND', {
|
|
40
|
+
title: 'No se encontró el archivo de configuración',
|
|
41
|
+
files: [file],
|
|
42
|
+
reason: 'Se pasó --config con una ruta que no existe.',
|
|
43
|
+
action: 'Verificá la ruta o quitá --config para autodetectarlo.',
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
static routeConflict(pattern, files) {
|
|
48
|
+
return new RessError('RESS_ROUTE_CONFLICT', {
|
|
49
|
+
title: 'Conflicto de ruta',
|
|
50
|
+
files,
|
|
51
|
+
reason: `Dos archivos declaran el patrón ${pattern}.`,
|
|
52
|
+
action: 'Renombrá uno de los dos archivos para que declaren patrones diferentes.',
|
|
53
|
+
exitCode: 1,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
static routePatternInvalid(pattern, file, reason) {
|
|
57
|
+
return new RessError('RESS_ROUTE_PATTERN_INVALID', {
|
|
58
|
+
title: 'Patrón de ruta inválido',
|
|
59
|
+
files: [file],
|
|
60
|
+
reason,
|
|
61
|
+
action: `Corregí el nombre de archivo que produce "${pattern}".`,
|
|
62
|
+
exitCode: 1,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
static buildMissing(outDir) {
|
|
66
|
+
return new RessError('RESS_BUILD_MISSING', {
|
|
67
|
+
title: 'No hay build de producción',
|
|
68
|
+
files: [outDir],
|
|
69
|
+
reason: `No se encontró ${outDir}.`,
|
|
70
|
+
action: 'Ejecutá `ress build` antes de `ress start`.',
|
|
71
|
+
exitCode: 3,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
static buildIncompatible(found, expected) {
|
|
75
|
+
return new RessError('RESS_BUILD_INCOMPATIBLE', {
|
|
76
|
+
title: 'El build es de una versión incompatible',
|
|
77
|
+
reason: `El manifest es de la versión ${found} y este ress.js espera la ${expected}.`,
|
|
78
|
+
action: 'Volvé a ejecutar `ress build`.',
|
|
79
|
+
exitCode: 3,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
static portInUse(port, process_) {
|
|
83
|
+
return new RessError('RESS_PORT_IN_USE', {
|
|
84
|
+
title: 'Puerto ocupado',
|
|
85
|
+
reason: process_
|
|
86
|
+
? `El puerto ${port} ya lo usa ${process_}.`
|
|
87
|
+
: `El puerto ${port} ya está en uso.`,
|
|
88
|
+
action: 'Elegí otro puerto con --port o pasá --no-strict-port para probar el siguiente libre.',
|
|
89
|
+
exitCode: 3,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
static serverFileInvalid(file) {
|
|
93
|
+
return new RessError('RESS_SERVER_FILE_INVALID', {
|
|
94
|
+
title: 'El archivo de servidor no tiene la forma esperada',
|
|
95
|
+
files: [file],
|
|
96
|
+
reason: 'Se esperaba una función por defecto que reciba { app, mode, options, manifest }.',
|
|
97
|
+
action: `Revisá ${file}: debe hacer \`export default function setup(ctx) { ... }\`.`,
|
|
98
|
+
exitCode: 1,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
static serverFileThrew(file, cause) {
|
|
102
|
+
return new RessError('RESS_SERVER_FILE_THREW', {
|
|
103
|
+
title: 'El archivo de servidor falló al cargarse',
|
|
104
|
+
files: [file],
|
|
105
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
106
|
+
action: 'Corregí el error en el archivo de servidor.',
|
|
107
|
+
exitCode: 1,
|
|
108
|
+
cause,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
static badUsage(reason) {
|
|
112
|
+
return new RessError('RESS_BAD_USAGE', {
|
|
113
|
+
title: 'Uso incorrecto',
|
|
114
|
+
reason,
|
|
115
|
+
action: 'Ejecutá `ress --help` para ver los comandos y argumentos disponibles.',
|
|
116
|
+
exitCode: 2,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { ModuleLoader, RouteManifest } from '@ressjs/vite-router';
|
|
3
|
+
import type { RessReporter } from './reporter.js';
|
|
4
|
+
import type { RessMode } from './types.js';
|
|
5
|
+
import type { ResolvedRessOptions } from './options.js';
|
|
6
|
+
/** Autodetección: el primero que exista, en este orden. */
|
|
7
|
+
export declare function findServerFile(root: string): Promise<string | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Resuelve qué archivo usar, si alguno.
|
|
10
|
+
*
|
|
11
|
+
* `serverFile.origin === '--server=false'` es la única forma de distinguir
|
|
12
|
+
* "no se pidió nada, autodetectá" de "se pidió explícitamente que no haya
|
|
13
|
+
* ninguno" cuando los dos resuelven `value: null` en `resolveOptions`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveServerFilePath(options: ResolvedRessOptions): Promise<string | null>;
|
|
16
|
+
export interface ApplyServerFileParams {
|
|
17
|
+
app: Express;
|
|
18
|
+
options: ResolvedRessOptions;
|
|
19
|
+
mode: RessMode;
|
|
20
|
+
manifest: RouteManifest;
|
|
21
|
+
vite?: unknown;
|
|
22
|
+
reporter: RessReporter;
|
|
23
|
+
/** Sólo en desarrollo: el cargador que ya construyó el runtime del framework. */
|
|
24
|
+
moduleLoader?: ModuleLoader;
|
|
25
|
+
/** Sólo en desarrollo: para reintentar cuando el archivo lanzó al cargarse. */
|
|
26
|
+
onSourceChange?: (cb: (file: string) => void) => () => void;
|
|
27
|
+
}
|
|
28
|
+
/** Aplica el archivo de servidor del proyecto, si existe. No hace nada si no hay ninguno. */
|
|
29
|
+
export declare function applyServerFile(params: ApplyServerFileParams): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vía de escape nivel 2 — archivo de servidor del proyecto.
|
|
3
|
+
*
|
|
4
|
+
* Si existe `server.{ts,tsx,js,mjs}` en la raíz, se carga y se le pasa la app
|
|
5
|
+
* ya construida por `createRessApp`. El archivo no construye nada: sólo recibe
|
|
6
|
+
* y opcionalmente devuelve.
|
|
7
|
+
*
|
|
8
|
+
* **Límite conocido, documentado y no resuelto por esta spec**: en producción
|
|
9
|
+
* sólo se soportan archivos `.js`/`.mjs`, cargados con `import()` nativo. Un
|
|
10
|
+
* `server.ts`/`.tsx` en producción requeriría que el pipeline de build de
|
|
11
|
+
* F-015 lo compilara como una entrada más — F-015 sólo compila páginas y
|
|
12
|
+
* middlewares bajo `app/pages`, no un archivo arbitrario en la raíz. Extender
|
|
13
|
+
* ese pipeline es trabajo de F-015, no de F-033: se falla con un mensaje que
|
|
14
|
+
* dice exactamente esto en vez de fingir que funciona.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs/promises';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { RessError } from './errors.js';
|
|
20
|
+
const CANDIDATE_EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs'];
|
|
21
|
+
/** Autodetección: el primero que exista, en este orden. */
|
|
22
|
+
export async function findServerFile(root) {
|
|
23
|
+
for (const ext of CANDIDATE_EXTENSIONS) {
|
|
24
|
+
const candidate = path.join(root, `server${ext}`);
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(candidate);
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resuelve qué archivo usar, si alguno.
|
|
37
|
+
*
|
|
38
|
+
* `serverFile.origin === '--server=false'` es la única forma de distinguir
|
|
39
|
+
* "no se pidió nada, autodetectá" de "se pidió explícitamente que no haya
|
|
40
|
+
* ninguno" cuando los dos resuelven `value: null` en `resolveOptions`.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveServerFilePath(options) {
|
|
43
|
+
const { serverFile, root } = options;
|
|
44
|
+
if (serverFile.value)
|
|
45
|
+
return path.resolve(root.value, serverFile.value);
|
|
46
|
+
if (serverFile.origin === '--server=false')
|
|
47
|
+
return null;
|
|
48
|
+
return findServerFile(root.value);
|
|
49
|
+
}
|
|
50
|
+
async function loadServerModule(file, root, mode, moduleLoader) {
|
|
51
|
+
if (mode === 'development' && moduleLoader) {
|
|
52
|
+
const id = '/' + path.relative(root, file).replace(/\\/g, '/');
|
|
53
|
+
return moduleLoader.load(id);
|
|
54
|
+
}
|
|
55
|
+
const ext = path.extname(file);
|
|
56
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
57
|
+
throw RessError.serverFileThrew(file, new Error('En producción sólo se soportan archivos .js/.mjs para el servidor del proyecto. ' +
|
|
58
|
+
'F-015 no compila todavía un archivo de servidor arbitrario fuera de app/pages: ' +
|
|
59
|
+
'escribí server.js o server.mjs, o esperá a que F-015 lo cubra.'));
|
|
60
|
+
}
|
|
61
|
+
return import(/* @vite-ignore */ pathToFileURL(file).href + `?t=${Date.now()}`);
|
|
62
|
+
}
|
|
63
|
+
function validateShape(mod, file) {
|
|
64
|
+
const fn = mod.default;
|
|
65
|
+
if (typeof fn !== 'function')
|
|
66
|
+
throw RessError.serverFileInvalid(file);
|
|
67
|
+
return fn;
|
|
68
|
+
}
|
|
69
|
+
/** Aplica el archivo de servidor del proyecto, si existe. No hace nada si no hay ninguno. */
|
|
70
|
+
export async function applyServerFile(params) {
|
|
71
|
+
const { app, options, mode, manifest, vite, reporter, moduleLoader, onSourceChange } = params;
|
|
72
|
+
const file = await resolveServerFilePath(options);
|
|
73
|
+
if (!file)
|
|
74
|
+
return;
|
|
75
|
+
const ctx = { app, mode, options, manifest, vite };
|
|
76
|
+
const attempt = async () => {
|
|
77
|
+
let mod;
|
|
78
|
+
try {
|
|
79
|
+
mod = await loadServerModule(file, options.root.value, mode, moduleLoader);
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
if (cause instanceof RessError)
|
|
83
|
+
throw cause;
|
|
84
|
+
throw RessError.serverFileThrew(file, cause);
|
|
85
|
+
}
|
|
86
|
+
const setup = validateShape(mod, file);
|
|
87
|
+
const result = await setup(ctx);
|
|
88
|
+
if (result)
|
|
89
|
+
Object.assign(app, result);
|
|
90
|
+
reporter.event({ type: 'server:extended', file, hook: 'file' });
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
await attempt();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (mode === 'production')
|
|
98
|
+
throw error;
|
|
99
|
+
// En desarrollo el proceso sigue vivo: se avisa y se reintenta cuando el
|
|
100
|
+
// archivo vuelva a cambiar. No hay nada montado todavía, así que el
|
|
101
|
+
// reintento es un primer montaje, no un remontaje.
|
|
102
|
+
const message = error instanceof RessError ? error.reason ?? error.title : String(error);
|
|
103
|
+
reporter.event({ type: 'warn', message: `server.* falló al cargarse: ${message}`, file });
|
|
104
|
+
if (!onSourceChange)
|
|
105
|
+
return;
|
|
106
|
+
const stop = onSourceChange((changed) => {
|
|
107
|
+
if (path.resolve(changed) !== path.resolve(file))
|
|
108
|
+
return;
|
|
109
|
+
attempt()
|
|
110
|
+
.then(() => stop())
|
|
111
|
+
.catch((retryError) => {
|
|
112
|
+
const retryMessage = retryError instanceof RessError ? retryError.reason ?? retryError.title : String(retryError);
|
|
113
|
+
reporter.event({ type: 'warn', message: `server.* volvió a fallar: ${retryMessage}`, file });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superficie pública de `@ressjs/cli`.
|
|
3
|
+
*
|
|
4
|
+
* Todo lo que hace un comando de la CLI está disponible acá como función. La
|
|
5
|
+
* CLI (`src/cli/**`) es un consumidor de esta superficie, no una segunda
|
|
6
|
+
* implementación.
|
|
7
|
+
*/
|
|
8
|
+
export { createRessApp } from './app.js';
|
|
9
|
+
export { startDevServer } from './dev.js';
|
|
10
|
+
export { startProdServer } from './start.js';
|
|
11
|
+
export { buildApp } from './build.js';
|
|
12
|
+
export { inspectProject, cliVersion } from './inspect.js';
|
|
13
|
+
export { resolveOptions } from './options.js';
|
|
14
|
+
export { silentReporter, collectingReporter } from './reporter.js';
|
|
15
|
+
export { RessError } from './errors.js';
|
|
16
|
+
export { findServerFile, resolveServerFilePath } from './extend.js';
|
|
17
|
+
export type { BuildAppOptions } from './build.js';
|
|
18
|
+
export type { ProjectInfo, InspectOptions } from './inspect.js';
|
|
19
|
+
export type { OptionSource, ResolvedOption, ResolvedRessOptions, ResolveDefaults, } from './options.js';
|
|
20
|
+
export type { RessEvent, RessReporter, VariantTag } from './reporter.js';
|
|
21
|
+
export type { RessErrorCode, RessErrorInit } from './errors.js';
|
|
22
|
+
export type { BuildResult, BuildTargetResult, LogLevel, RessApp, RessAppOptions, RessMode, RessServer, RessServerFile, RessServerFileContext, } from './types.js';
|