@exisjs/cli 0.0.0-stage → 0.10.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +35 -2
  2. package/bin/exisjs.js +2 -0
  3. package/dist/commands/build.d.ts +17 -0
  4. package/dist/commands/build.d.ts.map +1 -0
  5. package/dist/commands/build.js +378 -0
  6. package/dist/commands/build.js.map +1 -0
  7. package/dist/commands/clean.d.ts +5 -0
  8. package/dist/commands/clean.d.ts.map +1 -0
  9. package/dist/commands/clean.js +30 -0
  10. package/dist/commands/clean.js.map +1 -0
  11. package/dist/commands/dev.d.ts +28 -0
  12. package/dist/commands/dev.d.ts.map +1 -0
  13. package/dist/commands/dev.js +884 -0
  14. package/dist/commands/dev.js.map +1 -0
  15. package/dist/commands/doctor.d.ts +2 -0
  16. package/dist/commands/doctor.d.ts.map +1 -0
  17. package/dist/commands/doctor.js +228 -0
  18. package/dist/commands/doctor.js.map +1 -0
  19. package/dist/commands/exports.d.ts +2 -0
  20. package/dist/commands/exports.d.ts.map +1 -0
  21. package/dist/commands/exports.js +48 -0
  22. package/dist/commands/exports.js.map +1 -0
  23. package/dist/commands/generate.d.ts +33 -0
  24. package/dist/commands/generate.d.ts.map +1 -0
  25. package/dist/commands/generate.js +469 -0
  26. package/dist/commands/generate.js.map +1 -0
  27. package/dist/commands/init.d.ts +2 -0
  28. package/dist/commands/init.d.ts.map +1 -0
  29. package/dist/commands/init.js +37 -0
  30. package/dist/commands/init.js.map +1 -0
  31. package/dist/commands/migrate.d.ts +19 -0
  32. package/dist/commands/migrate.d.ts.map +1 -0
  33. package/dist/commands/migrate.js +116 -0
  34. package/dist/commands/migrate.js.map +1 -0
  35. package/dist/commands/repl.d.ts +6 -0
  36. package/dist/commands/repl.d.ts.map +1 -0
  37. package/dist/commands/repl.js +75 -0
  38. package/dist/commands/repl.js.map +1 -0
  39. package/dist/commands/run.d.ts +7 -0
  40. package/dist/commands/run.d.ts.map +1 -0
  41. package/dist/commands/run.js +101 -0
  42. package/dist/commands/run.js.map +1 -0
  43. package/dist/commands/start.d.ts +2 -0
  44. package/dist/commands/start.d.ts.map +1 -0
  45. package/dist/commands/start.js +8 -0
  46. package/dist/commands/start.js.map +1 -0
  47. package/dist/commands/test.d.ts +8 -0
  48. package/dist/commands/test.d.ts.map +1 -0
  49. package/dist/commands/test.js +166 -0
  50. package/dist/commands/test.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +276 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/lib/inspect-routes.d.ts +2 -0
  56. package/dist/lib/inspect-routes.d.ts.map +1 -0
  57. package/dist/lib/inspect-routes.js +150 -0
  58. package/dist/lib/inspect-routes.js.map +1 -0
  59. package/dist/lib/start-repl.d.ts +2 -0
  60. package/dist/lib/start-repl.d.ts.map +1 -0
  61. package/dist/lib/start-repl.js +120 -0
  62. package/dist/lib/start-repl.js.map +1 -0
  63. package/dist/manifest.d.ts +4 -0
  64. package/dist/manifest.d.ts.map +1 -0
  65. package/dist/manifest.js +489 -0
  66. package/dist/manifest.js.map +1 -0
  67. package/dist/routes.d.ts +17 -0
  68. package/dist/routes.d.ts.map +1 -0
  69. package/dist/routes.js +316 -0
  70. package/dist/routes.js.map +1 -0
  71. package/dist/utils.d.ts +24 -0
  72. package/dist/utils.d.ts.map +1 -0
  73. package/dist/utils.js +54 -0
  74. package/dist/utils.js.map +1 -0
  75. package/package.json +58 -4
@@ -0,0 +1,884 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.RELOADABLE_CONFIG_FILES = exports.RELOADABLE_EXTENSIONS = exports.DEFAULT_WATCH_IGNORED = void 0;
40
+ exports.isClosedChannelError = isClosedChannelError;
41
+ exports.watchIgnore = watchIgnore;
42
+ exports.shouldReloadOnFile = shouldReloadOnFile;
43
+ exports.isDevLockHeld = isDevLockHeld;
44
+ exports.devCommand = devCommand;
45
+ const node_child_process_1 = require("node:child_process");
46
+ const node_path_1 = __importDefault(require("node:path"));
47
+ const node_fs_1 = __importDefault(require("node:fs"));
48
+ const node_http_1 = __importDefault(require("node:http"));
49
+ const node_net_1 = __importDefault(require("node:net"));
50
+ const node_url_1 = require("node:url");
51
+ const utils_1 = require("../utils");
52
+ const internal_1 = require("exisjs/internal");
53
+ const ENV_FILES = [
54
+ '.env',
55
+ '.env.local',
56
+ '.env.development',
57
+ '.env.development.local',
58
+ ];
59
+ const CLOSED_CHANNEL_CODES = [
60
+ 'EPIPE',
61
+ 'ERR_IPC_CHANNEL_CLOSED',
62
+ 'ERR_IPC_DISCONNECTED',
63
+ ];
64
+ function isClosedChannelError(err) {
65
+ return !!err && CLOSED_CHANNEL_CODES.includes(err.code ?? '');
66
+ }
67
+ // child.send() callback: a child that already exited is expected at shutdown
68
+ function ignoreClosedChannel(err) {
69
+ if (err && !isClosedChannelError(err)) {
70
+ console.error(`[exis] Could not signal the server process: ${err.message}`);
71
+ }
72
+ }
73
+ exports.DEFAULT_WATCH_IGNORED = [
74
+ // Dotfiles and dotfolders (.git, .vscode, .idea, etc.) except .env
75
+ /(^|[/\\])\.(?!env)/,
76
+ /(^|[/\\])node_modules([/\\]|$)/,
77
+ /(^|[/\\])\.exis([/\\]|$)/,
78
+ /(^|[/\\])dist([/\\]|$)/,
79
+ /(^|[/\\])build([/\\]|$)/,
80
+ /(^|[/\\])out([/\\]|$)/,
81
+ /(^|[/\\])\.turbo([/\\]|$)/,
82
+ /(^|[/\\])\.next([/\\]|$)/,
83
+ /(^|[/\\])\.cache([/\\]|$)/,
84
+ // Lock files
85
+ /(^|[/\\])(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|bun\.lock)$/i,
86
+ // Tests & Coverage
87
+ /(^|[/\\])(tests?|coverage|__tests__|\.nyc_output)([/\\]|$)/i,
88
+ /\.(test|spec)\.[tj]sx?$/i,
89
+ // Generated types
90
+ /exis\.d\.ts$/,
91
+ /\.d\.ts$/,
92
+ // Runtime data, storage, downloads, artifacts, uploads, logs, temp dirs
93
+ /(^|[/\\])(downloads|storage|uploads|tmp|temp|logs|data|fixtures|reports|artifacts|public)([/\\]|$)/i,
94
+ // Non-source & runtime artifact file extensions
95
+ /\.(rar|zip|7z|tar|gz|tgz|bz2|xz|iso)$/i, // compressed archives
96
+ /\.(bak|tmp|temp|swp|swo|lock|pid)$/i, // temporary and lock files
97
+ /\.(log|log\.\d+|sqlite|sqlite3|db|db-shm|db-wal|db-journal)$/i, // logs and databases
98
+ /\.(png|jpe?g|gif|svg|ico|webp|avif|mp4|webm|mov|mp3|wav|ogg)$/i, // media files
99
+ /\.(pdf|docx?|xlsx?|pptx?|csv|tsv)$/i, // documents & spreadsheets
100
+ /\.(md|mdx|txt|rst|adoc)$/i, // markdown & plain text documentation
101
+ ];
102
+ exports.RELOADABLE_EXTENSIONS = new Set([
103
+ '.ts',
104
+ '.tsx',
105
+ '.js',
106
+ '.jsx',
107
+ '.mjs',
108
+ '.cjs',
109
+ ]);
110
+ exports.RELOADABLE_CONFIG_FILES = new Set([
111
+ 'tsconfig.json',
112
+ 'package.json',
113
+ 'exis.config.ts',
114
+ 'exis.config.js',
115
+ 'exis.config.mjs',
116
+ 'exis.config.cjs',
117
+ ]);
118
+ /**
119
+ * chokidar's `ignored` test. Patterns apply to the path inside the project:
120
+ * a project under a folder such as `D:\data` or `/srv/build` must still
121
+ * reload, which matching the absolute path prevented.
122
+ */
123
+ function watchIgnore(cwd, userIgnored = []) {
124
+ const patterns = [...exports.DEFAULT_WATCH_IGNORED, ...userIgnored];
125
+ return (file) => {
126
+ const rel = node_path_1.default
127
+ .relative(cwd, node_path_1.default.resolve(cwd, file))
128
+ .split(node_path_1.default.sep)
129
+ .join('/');
130
+ if (rel === '' ||
131
+ rel === '..' ||
132
+ rel.startsWith('../') ||
133
+ node_path_1.default.isAbsolute(rel)) {
134
+ return false;
135
+ }
136
+ return patterns.some((p) => typeof p === 'string'
137
+ ? rel.includes(p) || node_path_1.default.basename(rel) === p
138
+ : p.test(rel));
139
+ };
140
+ }
141
+ function shouldReloadOnFile(file, userIgnored = [], customExtensions) {
142
+ if (!file)
143
+ return false;
144
+ const normalized = file.replace(/\\/g, '/');
145
+ const baseName = node_path_1.default.basename(normalized);
146
+ const ext = node_path_1.default.extname(normalized).toLowerCase();
147
+ // 1. Check user custom ignores
148
+ for (const pattern of userIgnored) {
149
+ if (typeof pattern === 'string') {
150
+ if (normalized.includes(pattern) || baseName === pattern)
151
+ return false;
152
+ }
153
+ else if (pattern instanceof RegExp && pattern.test(normalized)) {
154
+ return false;
155
+ }
156
+ }
157
+ // 2. Check default ignored patterns
158
+ for (const regex of exports.DEFAULT_WATCH_IGNORED) {
159
+ if (typeof regex !== 'string' && regex.test(normalized))
160
+ return false;
161
+ }
162
+ // 3. Environment files (.env, .env.local, .env.development, etc.)
163
+ if (baseName === '.env' || baseName.startsWith('.env.')) {
164
+ return true;
165
+ }
166
+ // 4. Reloadable config files
167
+ if (exports.RELOADABLE_CONFIG_FILES.has(baseName)) {
168
+ return true;
169
+ }
170
+ // 5. Custom allowed extensions if specified by user
171
+ if (customExtensions && customExtensions.length > 0) {
172
+ const extSet = new Set(customExtensions.map((e) => e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`));
173
+ return extSet.has(ext);
174
+ }
175
+ // 6. Source file extensions
176
+ if (exports.RELOADABLE_EXTENSIONS.has(ext)) {
177
+ // Avoid re-triggering for declaration files like index.d.ts
178
+ if (normalized.endsWith('.d.ts'))
179
+ return false;
180
+ return true;
181
+ }
182
+ // Any other file (e.g. .csv, .xlsx, .md, .txt, .json data file) is ignored
183
+ return false;
184
+ }
185
+ /** True when something accepts connections on the port. */
186
+ function isPortInUse(port) {
187
+ return new Promise((resolve) => {
188
+ const socket = node_net_1.default.connect({ port, host: '127.0.0.1' });
189
+ socket.setTimeout(500);
190
+ socket.once('connect', () => {
191
+ socket.destroy();
192
+ resolve(true);
193
+ });
194
+ socket.once('timeout', () => {
195
+ socket.destroy();
196
+ resolve(false);
197
+ });
198
+ socket.once('error', () => resolve(false));
199
+ });
200
+ }
201
+ /**
202
+ * Another `exisjs dev` holds this lock when its PID is alive AND it still
203
+ * serves the port it recorded. A PID alone is not enough: after a crash the
204
+ * OS may hand the same number to an unrelated process.
205
+ */
206
+ async function isDevLockHeld(lockFile) {
207
+ let lock;
208
+ try {
209
+ lock = JSON.parse(node_fs_1.default.readFileSync(lockFile, 'utf-8'));
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ if (!lock.pid || lock.pid === process.pid || !lock.port)
215
+ return false;
216
+ try {
217
+ process.kill(lock.pid, 0);
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ return isPortInUse(lock.port);
223
+ }
224
+ async function devCommand(options = {}) {
225
+ const cwd = process.cwd();
226
+ const startServerPath = internal_1.START_SERVER_PATH;
227
+ // Detect entry file
228
+ const entryFile = resolveEntry(cwd, options.entry);
229
+ if (!entryFile) {
230
+ (0, utils_1.error)('Could not find entry file. Expected src/http/server.ts or http/server.ts');
231
+ (0, utils_1.error)('Run this command from your Exis JS project root.');
232
+ process.exit(1);
233
+ }
234
+ const exisDir = node_path_1.default.join(cwd, '.exis');
235
+ // 2. Load .env files early so process.env.PORT is available
236
+ (0, internal_1.loadEnv)(cwd);
237
+ // 3. Read exis.config.ts to pick up user-defined port
238
+ const userConfig = await (0, internal_1.loadConfig)(cwd);
239
+ // 4. Automatic Port Finding — priority: CLI flag > env > config > default(4000)
240
+ const startPort = parseInt(options.port || process.env.PORT || String(userConfig.port) || '4000', 10);
241
+ function findAvailablePort(port) {
242
+ return new Promise((resolve) => {
243
+ const server = node_net_1.default.createServer();
244
+ server.listen(port, () => {
245
+ server.once('close', () => resolve(port));
246
+ server.close();
247
+ });
248
+ server.on('error', () => {
249
+ resolve(findAvailablePort(port + 1));
250
+ });
251
+ });
252
+ }
253
+ // 1. One dev server per port: a second instance on another port (for
254
+ // example pointed at a test database) is allowed
255
+ const pidFile = node_path_1.default.join(exisDir, `dev-${startPort}.pid`);
256
+ if (!options.force && (await isDevLockHeld(pidFile))) {
257
+ console.error(`\n\x1b[31m[ExisJS] A development server for port ${startPort} is already running in another terminal.\x1b[0m\n` +
258
+ 'Stop it, start this one on another port (--port), or pass --force.');
259
+ process.exit(1);
260
+ }
261
+ const actualPort = await findAvailablePort(startPort);
262
+ if (!node_fs_1.default.existsSync(exisDir))
263
+ node_fs_1.default.mkdirSync(exisDir, { recursive: true });
264
+ node_fs_1.default.writeFileSync(pidFile, JSON.stringify({ pid: process.pid, port: actualPort }));
265
+ // Check for tsconfig (only warn for TypeScript projects)
266
+ const tsconfigPath = node_path_1.default.join(cwd, 'tsconfig.json');
267
+ if (!node_fs_1.default.existsSync(tsconfigPath) && entryFile.endsWith('.ts')) {
268
+ (0, utils_1.warn)('No tsconfig.json found — using default TypeScript settings.');
269
+ }
270
+ process.env.PORT = String(actualPort);
271
+ if (options.host) {
272
+ process.env.HOST = options.host;
273
+ }
274
+ // Built for every start of the app process, so edits to .env files apply
275
+ // on the next reload instead of only after restarting `exisjs dev`
276
+ const buildChildEnv = () => {
277
+ (0, internal_1.loadEnv)(cwd, undefined, true);
278
+ process.env.PORT = String(actualPort);
279
+ if (options.host)
280
+ process.env.HOST = options.host;
281
+ return {
282
+ ...process.env,
283
+ NODE_ENV: 'development',
284
+ FORCE_COLOR: '1',
285
+ };
286
+ };
287
+ let version = '0.0.0';
288
+ try {
289
+ const pkgPath = node_path_1.default.join(__dirname, '..', '..', 'package.json');
290
+ version = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8')).version;
291
+ }
292
+ catch {
293
+ /* ignore */
294
+ }
295
+ const primary = '\x1b[38;2;160;70;255m';
296
+ const time = (0, internal_1.getFormattedTime)();
297
+ console.log(`\n${utils_1.c.dim}${time}${utils_1.c.reset} ${primary}[exis]${utils_1.c.reset} ${utils_1.c.dim}starting development server (v${version})...${utils_1.c.reset}`);
298
+ // Hardcode Esbuild (tsx) as the Native HMR runner
299
+ let tsxArgs = [require.resolve('tsx/cli')];
300
+ try {
301
+ const resolved = require.resolve('tsx', { paths: [cwd, __dirname] });
302
+ tsxArgs = ['--import', (0, node_url_1.pathToFileURL)(resolved).href];
303
+ }
304
+ catch {
305
+ /* fallback to tsx/cli if resolve fails */
306
+ }
307
+ const runner = {
308
+ name: 'tsx',
309
+ bin: process.execPath,
310
+ args: tsxArgs,
311
+ needsManualWatch: true,
312
+ };
313
+ let child = null;
314
+ let isShuttingDown = false;
315
+ let watcher = null;
316
+ function cleanupPid() {
317
+ try {
318
+ if (node_fs_1.default.existsSync(pidFile)) {
319
+ node_fs_1.default.unlinkSync(pidFile);
320
+ }
321
+ }
322
+ catch {
323
+ /* ignore */
324
+ }
325
+ }
326
+ let forceExitTriggered = false;
327
+ async function handleSessionStop(_signal) {
328
+ if (isShuttingDown) {
329
+ if (!forceExitTriggered) {
330
+ forceExitTriggered = true;
331
+ cleanupPid();
332
+ if (child && child.pid) {
333
+ try {
334
+ if (process.platform === 'win32') {
335
+ (0, node_child_process_1.spawnSync)('taskkill', ['/pid', String(child.pid), '/f', '/t'], {
336
+ stdio: 'ignore',
337
+ });
338
+ }
339
+ else {
340
+ child.kill('SIGKILL');
341
+ }
342
+ }
343
+ catch {
344
+ /* ignore */
345
+ }
346
+ }
347
+ process.exit(0);
348
+ }
349
+ return;
350
+ }
351
+ isShuttingDown = true;
352
+ cleanupPid();
353
+ const time = (0, internal_1.getFormattedTime)();
354
+ const primary = '\x1b[38;2;160;70;255m';
355
+ console.log(`\n${utils_1.c.dim}${time}${utils_1.c.reset} ${primary}[exis]${utils_1.c.reset} ${utils_1.c.dim}gracefully shutting down server...${utils_1.c.reset}`);
356
+ if (watcher) {
357
+ try {
358
+ watcher.close();
359
+ }
360
+ catch {
361
+ /* ignore */
362
+ }
363
+ watcher = null;
364
+ }
365
+ if (global._tscProcess) {
366
+ try {
367
+ ;
368
+ global._tscProcess.kill();
369
+ }
370
+ catch {
371
+ /* ignore */
372
+ }
373
+ }
374
+ if (child &&
375
+ child.pid &&
376
+ child.exitCode === null &&
377
+ child.signalCode === null) {
378
+ try {
379
+ if (child.connected) {
380
+ // The callback receives the error if the child already closed its end
381
+ child.send({ type: 'exis:shutdown' }, ignoreClosedChannel);
382
+ child.disconnect();
383
+ }
384
+ }
385
+ catch {
386
+ /* ignore */
387
+ }
388
+ try {
389
+ if (process.platform !== 'win32') {
390
+ child.kill('SIGTERM');
391
+ }
392
+ }
393
+ catch {
394
+ /* ignore */
395
+ }
396
+ const exitTimeout = setTimeout(() => {
397
+ try {
398
+ if (child && !child.killed) {
399
+ if (process.platform === 'win32') {
400
+ (0, node_child_process_1.spawnSync)('taskkill', ['/pid', String(child.pid), '/f', '/t'], {
401
+ stdio: 'ignore',
402
+ });
403
+ }
404
+ else {
405
+ child.kill('SIGKILL');
406
+ }
407
+ }
408
+ }
409
+ catch {
410
+ /* ignore */
411
+ }
412
+ process.exit(0);
413
+ }, 2000);
414
+ const onChildDone = () => {
415
+ clearTimeout(exitTimeout);
416
+ process.exit(0);
417
+ };
418
+ child.once('close', onChildDone);
419
+ child.once('exit', onChildDone);
420
+ }
421
+ else {
422
+ process.exit(0);
423
+ }
424
+ }
425
+ process.on('SIGINT', () => handleSessionStop('SIGINT'));
426
+ process.on('SIGTERM', () => handleSessionStop('SIGTERM'));
427
+ let fallbackServer = null;
428
+ function closeFallbackServer() {
429
+ if (fallbackServer) {
430
+ try {
431
+ fallbackServer.close();
432
+ }
433
+ catch {
434
+ /* ignore */
435
+ }
436
+ fallbackServer = null;
437
+ }
438
+ }
439
+ let configuredPort = null;
440
+ try {
441
+ const configTs = node_path_1.default.join(cwd, 'exis.config.ts');
442
+ const configJs = node_path_1.default.join(cwd, 'exis.config.js');
443
+ const configFile = node_fs_1.default.existsSync(configTs)
444
+ ? configTs
445
+ : node_fs_1.default.existsSync(configJs)
446
+ ? configJs
447
+ : null;
448
+ if (configFile) {
449
+ const content = node_fs_1.default.readFileSync(configFile, 'utf8');
450
+ const match = content.match(/\bport\s*:\s*(\d+)/);
451
+ if (match) {
452
+ configuredPort = parseInt(match[1], 10);
453
+ }
454
+ }
455
+ }
456
+ catch {
457
+ /* ignore */
458
+ }
459
+ function startFallbackServer(errorMessage, customPort) {
460
+ closeFallbackServer();
461
+ const port = customPort ??
462
+ (process.env.PORT ? parseInt(process.env.PORT, 10) : configuredPort) ??
463
+ 4000;
464
+ // ANSI colour codes belong to the terminal, not to a response
465
+ const plainMessage = errorMessage.replace(new RegExp(String.fromCharCode(27) + '\\[[0-9;]*[A-Za-z]', 'g'), '');
466
+ fallbackServer = node_http_1.default.createServer((req, res) => {
467
+ // API clients, curl and health checks get the standard JSON error;
468
+ // only browsers asking for HTML get the page
469
+ if (!String(req.headers.accept || '').includes('text/html')) {
470
+ res.writeHead(503, {
471
+ 'Content-Type': 'application/json; charset=utf-8',
472
+ 'Retry-After': '1',
473
+ });
474
+ res.end(JSON.stringify({
475
+ success: false,
476
+ statusCode: 503,
477
+ error: {
478
+ code: 'BUILD_ERROR',
479
+ message: 'The app failed to start. Fix the error and save to reload.',
480
+ details: plainMessage.trim().split('\n').slice(-40),
481
+ },
482
+ }));
483
+ return;
484
+ }
485
+ res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8' });
486
+ res.end(`
487
+ <!DOCTYPE html>
488
+ <html lang="en">
489
+ <head>
490
+ <meta charset="UTF-8">
491
+ <title>Build Error | Exis</title>
492
+ <style>
493
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #fcebeb; color: #333; margin: 0; padding: 40px; }
494
+ .container { max-width: 900px; margin: 0 auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 10px 25px rgba(200, 0, 0, 0.1); border-left: 6px solid #e53e3e; }
495
+ h1 { color: #e53e3e; margin-top: 0; font-size: 24px; }
496
+ .code-block { background: #1e1e1e; color: #fc8181; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
497
+ </style>
498
+ </head>
499
+ <body>
500
+ <div class="container">
501
+ <h1>Build/Syntax Error</h1>
502
+ <div class="code-block">${plainMessage.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
503
+ </div>
504
+ </body>
505
+ </html>
506
+ `);
507
+ });
508
+ // Same interface as the app itself, never wider
509
+ const host = options.host || process.env.HOST || userConfig.host || 'localhost';
510
+ fallbackServer
511
+ .listen(port, host, () => {
512
+ console.log(`\n${utils_1.c.red}[exis] Serving build error on port ${port}${utils_1.c.reset}`);
513
+ })
514
+ .on('error', () => {
515
+ // Port might still be in use by zombie process, just ignore
516
+ });
517
+ }
518
+ let isRestarting = false;
519
+ let pendingRestart = false;
520
+ let reloadDebounceTimer = null;
521
+ const queuedChangedFiles = new Set();
522
+ const watchConfig = userConfig.watch || {};
523
+ const devConfig = userConfig.dev || {};
524
+ const userIgnored = [
525
+ ...(watchConfig.ignore || []),
526
+ ...(devConfig.ignored || []),
527
+ ];
528
+ const customExtensions = watchConfig.extensions || devConfig.extensions;
529
+ const RELOAD_DEBOUNCE_MS = watchConfig.debounceMs ?? devConfig.debounceMs ?? 250;
530
+ const RELOAD_TIMEOUT_MS = 3000;
531
+ async function killChildGracefully() {
532
+ if (!child || child.killed)
533
+ return;
534
+ // 1. Send IPC shutdown message so the child can run app.close() / onClose hooks
535
+ try {
536
+ if (child.connected) {
537
+ // The callback receives the error if the child already closed its end
538
+ child.send({ type: 'exis:shutdown' }, ignoreClosedChannel);
539
+ }
540
+ }
541
+ catch {
542
+ /* ignore — child may have already exited */
543
+ }
544
+ // 2. Disconnect the IPC channel IMMEDIATELY after sending the shutdown message.
545
+ // On Windows, this is CRITICAL: it closes the parent's end of the IPC pipe
546
+ // before the child starts tearing down its libuv handles, preventing the
547
+ // UV_HANDLE_CLOSING assertion crash in src\win\async.c.
548
+ try {
549
+ if (child.connected) {
550
+ child.disconnect();
551
+ }
552
+ }
553
+ catch {
554
+ /* ignore — channel may already be closed */
555
+ }
556
+ // 3. On non-Windows, also send SIGTERM as a direct signal
557
+ try {
558
+ if (process.platform !== 'win32') {
559
+ child.kill('SIGTERM');
560
+ }
561
+ }
562
+ catch {
563
+ /* ignore */
564
+ }
565
+ if (child) {
566
+ ;
567
+ child._isIntentionalKill = true;
568
+ }
569
+ // 4. Wait for clean exit with a generous graceful timeout (3000ms).
570
+ // Child will usually exit in ~50-200ms once onClose finishes.
571
+ await new Promise((resolve) => {
572
+ let resolved = false;
573
+ const onExitOrClose = () => {
574
+ if (resolved)
575
+ return;
576
+ resolved = true;
577
+ clearTimeout(timer);
578
+ resolve();
579
+ };
580
+ const timer = setTimeout(() => {
581
+ if (resolved)
582
+ return;
583
+ resolved = true;
584
+ // Timeout expired — force kill the child process tree
585
+ if (child && !child.killed) {
586
+ try {
587
+ if (process.platform === 'win32') {
588
+ (0, node_child_process_1.spawnSync)('taskkill', ['/pid', String(child.pid), '/f', '/t'], {
589
+ stdio: 'ignore',
590
+ });
591
+ }
592
+ else {
593
+ child.kill('SIGKILL');
594
+ }
595
+ }
596
+ catch {
597
+ /* ignore */
598
+ }
599
+ }
600
+ resolve();
601
+ }, RELOAD_TIMEOUT_MS);
602
+ child.once('close', onExitOrClose);
603
+ child.once('exit', onExitOrClose);
604
+ });
605
+ }
606
+ async function startProcess() {
607
+ if (isRestarting) {
608
+ pendingRestart = true;
609
+ return;
610
+ }
611
+ isRestarting = true;
612
+ closeFallbackServer();
613
+ await killChildGracefully();
614
+ child = (0, node_child_process_1.spawn)(runner.bin, [...runner.args, startServerPath], {
615
+ cwd,
616
+ env: {
617
+ ...buildChildEnv(),
618
+ __EXIS_DEV_SERVER: '1',
619
+ __EXIS_IS_RESTART: global._hasStartedBefore ? '1' : '',
620
+ EXIS_ENTRY_FILE: entryFile,
621
+ },
622
+ stdio: ['inherit', 'inherit', 'pipe', 'ipc'],
623
+ shell: runner.bin === process.execPath ? false : true,
624
+ });
625
+ global._hasStartedBefore = true;
626
+ let stderrBuffer = '';
627
+ const currentSpawnedChild = child;
628
+ child.stderr?.on('data', (chunk) => {
629
+ const str = chunk.toString();
630
+ process.stderr.write(chunk);
631
+ stderrBuffer += str;
632
+ // Keep only last 10000 chars to avoid memory leak
633
+ if (stderrBuffer.length > 10000) {
634
+ stderrBuffer = stderrBuffer.substring(stderrBuffer.length - 10000);
635
+ }
636
+ });
637
+ child.on('error', (err) => {
638
+ // Ctrl+C on Windows reaches the child too, so its IPC end can close
639
+ // while the parent is still telling it to stop: not a failure
640
+ if ((isShuttingDown || isRestarting) && isClosedChannelError(err))
641
+ return;
642
+ (0, utils_1.error)(currentSpawnedChild?.pid
643
+ ? `Server process error: ${err.message}`
644
+ : `Failed to start process: ${err.message}`);
645
+ });
646
+ child.on('exit', (code, signal) => {
647
+ if (currentSpawnedChild._isIntentionalKill) {
648
+ return;
649
+ }
650
+ if (!isShuttingDown &&
651
+ signal !== 'SIGTERM' &&
652
+ code !== 0 &&
653
+ code !== null) {
654
+ if (code === 128 + 15 || code === 143) {
655
+ // Normal sigterm exits
656
+ return;
657
+ }
658
+ (0, utils_1.error)(`Process crashed with code ${code}`);
659
+ // Start the fallback server so the browser doesn't hang or get "Connection Refused"
660
+ startFallbackServer(stderrBuffer || 'Process crashed with no error output.');
661
+ }
662
+ });
663
+ isRestarting = false;
664
+ // If more file changes arrived while we were restarting, trigger consolidated reload
665
+ if (pendingRestart) {
666
+ pendingRestart = false;
667
+ startProcess();
668
+ }
669
+ }
670
+ function scheduleReload(file) {
671
+ if (file) {
672
+ queuedChangedFiles.add(file);
673
+ }
674
+ if (reloadDebounceTimer) {
675
+ clearTimeout(reloadDebounceTimer);
676
+ }
677
+ reloadDebounceTimer = setTimeout(async () => {
678
+ reloadDebounceTimer = null;
679
+ const files = Array.from(queuedChangedFiles);
680
+ queuedChangedFiles.clear();
681
+ const time = (0, internal_1.getFormattedTime)();
682
+ const primary = '\x1b[38;2;160;70;255m';
683
+ const fileLabel = files.length === 1
684
+ ? files[0]
685
+ : files.length > 1
686
+ ? `${files[0]} (+${files.length - 1} other files)`
687
+ : 'file changes';
688
+ console.log(`${utils_1.c.dim}${time}${utils_1.c.reset} ${primary}[exis]${utils_1.c.reset} ${utils_1.c.dim}reloading due to change in ${fileLabel}${utils_1.c.reset}`);
689
+ await generateManifest(cwd, '', true);
690
+ startProcess();
691
+ }, RELOAD_DEBOUNCE_MS);
692
+ }
693
+ // Handle restarts for runners that don't support watch natively
694
+ if (runner.needsManualWatch && !options._disableWatch) {
695
+ const chokidar = await importChokidar();
696
+ if (chokidar) {
697
+ const configuredWatchPaths = watchConfig.paths || devConfig.watch || [];
698
+ let watchPaths;
699
+ if (configuredWatchPaths.length > 0) {
700
+ watchPaths = configuredWatchPaths;
701
+ }
702
+ else {
703
+ const srcExists = node_fs_1.default.existsSync(node_path_1.default.join(cwd, 'src'));
704
+ const httpExists = node_fs_1.default.existsSync(node_path_1.default.join(cwd, 'http'));
705
+ if (srcExists || httpExists) {
706
+ const candidates = [
707
+ srcExists ? 'src' : '',
708
+ httpExists ? 'http' : '',
709
+ ].filter(Boolean);
710
+ watchPaths = [
711
+ ...candidates.map((d) => node_path_1.default.join(cwd, d)),
712
+ ...[
713
+ 'exis.config.ts',
714
+ 'exis.config.js',
715
+ 'exis.config.mjs',
716
+ 'exis.config.cjs',
717
+ 'tsconfig.json',
718
+ 'package.json',
719
+ ]
720
+ .map((f) => node_path_1.default.join(cwd, f))
721
+ .filter((f) => node_fs_1.default.existsSync(f)),
722
+ ];
723
+ }
724
+ else {
725
+ watchPaths = [cwd];
726
+ }
727
+ }
728
+ // .env files are read when the app process starts, so a change needs
729
+ // a restart; watched even when `watch.paths` is set, and before they
730
+ // exist, so creating one reloads too
731
+ watchPaths.push(...ENV_FILES.map((f) => node_path_1.default.join(cwd, f)).filter((f) => !watchPaths.includes(f)));
732
+ watcher = chokidar.watch(watchPaths, {
733
+ cwd,
734
+ ignoreInitial: true,
735
+ usePolling: devConfig.usePolling ?? process.platform === 'win32',
736
+ interval: devConfig.interval ?? 100,
737
+ binaryInterval: devConfig.binaryInterval ?? 300,
738
+ ignored: watchIgnore(cwd, userIgnored),
739
+ awaitWriteFinish: {
740
+ stabilityThreshold: 200,
741
+ pollInterval: 100,
742
+ },
743
+ });
744
+ watcher.on('error', (err) => {
745
+ // Suppress non-fatal Windows file lock / permission errors (EBUSY / EPERM)
746
+ if (err?.code === 'EBUSY' ||
747
+ err?.code === 'EPERM' ||
748
+ err?.code === 'UNKNOWN') {
749
+ return;
750
+ }
751
+ console.warn(`\x1b[33m[exis watcher notice]\x1b[0m ${err?.message || err}`);
752
+ });
753
+ watcher.on('all', async (_eventName, file) => {
754
+ if (!shouldReloadOnFile(file, userIgnored, customExtensions)) {
755
+ return;
756
+ }
757
+ scheduleReload(file);
758
+ });
759
+ }
760
+ }
761
+ // Generate dev manifest for O(1) booting
762
+ const { generateManifest } = await Promise.resolve().then(() => __importStar(require('../manifest')));
763
+ await generateManifest(cwd, '', true);
764
+ if (node_fs_1.default.existsSync(tsconfigPath)) {
765
+ let tscPath;
766
+ try {
767
+ const req = typeof require !== 'undefined' ? require : eval('require');
768
+ tscPath = req.resolve('typescript/bin/tsc', { paths: [cwd] });
769
+ }
770
+ catch {
771
+ (0, utils_1.error)('TypeScript compiler not found in project.');
772
+ (0, utils_1.error)('Install it: npm install -D typescript');
773
+ process.exit(1);
774
+ }
775
+ const tscProcess = (0, node_child_process_1.spawn)(process.execPath, [tscPath, '--noEmit', '--watch', '--preserveWatchOutput'], { cwd, env: process.env });
776
+ global._tscProcess = tscProcess;
777
+ let tscErrors = [];
778
+ tscProcess.stdout?.on('data', (data) => {
779
+ const lines = data.toString().trim().split('\n');
780
+ const getTime = () => {
781
+ return (0, internal_1.getFormattedTime)();
782
+ };
783
+ for (const line of lines) {
784
+ if (!line)
785
+ continue;
786
+ if (line.includes('Found 0 errors')) {
787
+ tscErrors = [];
788
+ // Type errors are cleared — close the fallback so the real server can serve
789
+ closeFallbackServer();
790
+ }
791
+ else if (line.includes('error TS')) {
792
+ const time = getTime();
793
+ console.error(`\n${utils_1.c.red}${time} ERROR:${utils_1.c.reset} ${line}`);
794
+ tscErrors.push(line);
795
+ }
796
+ else if (line.includes('Found') && line.includes('error')) {
797
+ // "Found X errors." summary line
798
+ const time = getTime();
799
+ console.error(`\n${utils_1.c.yellow}${time} INFO:${utils_1.c.reset} ${line}`);
800
+ if (tscErrors.length > 0) {
801
+ startFallbackServer(`TypeScript Compilation Errors\n${'─'.repeat(50)}\n\n` +
802
+ tscErrors.join('\n'));
803
+ }
804
+ }
805
+ else if (!line.includes('Starting compilation in watch mode') &&
806
+ !line.includes('File change detected')) {
807
+ const time = getTime();
808
+ console.error(`${utils_1.c.yellow}${time} INFO:${utils_1.c.reset} ${line}`);
809
+ }
810
+ }
811
+ });
812
+ }
813
+ startProcess();
814
+ // CLI Shortcuts
815
+ if (process.stdin.isTTY) {
816
+ process.stdin.setEncoding('utf8');
817
+ process.stdin.on('data', (data) => {
818
+ const key = data.toString().trim().toLowerCase();
819
+ if (key === 'h') {
820
+ console.log(`
821
+ ${utils_1.c.bold}Shortcuts${utils_1.c.reset}
822
+ ${utils_1.c.dim}press${utils_1.c.reset} ${utils_1.c.bold}r + enter${utils_1.c.reset} ${utils_1.c.dim}to restart the server${utils_1.c.reset}
823
+ ${utils_1.c.dim}press${utils_1.c.reset} ${utils_1.c.bold}u + enter${utils_1.c.reset} ${utils_1.c.dim}to show server url${utils_1.c.reset}
824
+ ${utils_1.c.dim}press${utils_1.c.reset} ${utils_1.c.bold}c + enter${utils_1.c.reset} ${utils_1.c.dim}to clear console${utils_1.c.reset}
825
+ ${utils_1.c.dim}press${utils_1.c.reset} ${utils_1.c.bold}q + enter${utils_1.c.reset} ${utils_1.c.dim}to quit${utils_1.c.reset}
826
+ `);
827
+ }
828
+ else if (key === 'r') {
829
+ const time = (0, internal_1.getFormattedTime)();
830
+ console.log(`\n${utils_1.c.dim}${time}${utils_1.c.reset} ${primary}[exis]${utils_1.c.reset} ${utils_1.c.dim}restarting server...${utils_1.c.reset}`);
831
+ startProcess();
832
+ }
833
+ else if (key === 'c') {
834
+ console.clear();
835
+ }
836
+ else if (key === 'q') {
837
+ handleSessionStop('SIGINT');
838
+ }
839
+ else if (key === 'u') {
840
+ // Just trigger a dummy reload so app.ts prints the URL again
841
+ // Or send an IPC message, but since child.kill('SIGUSR2') isn't set up, we can just restart it
842
+ startProcess();
843
+ }
844
+ });
845
+ process.stdin.unref();
846
+ }
847
+ }
848
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
849
+ function resolveEntry(cwd, custom) {
850
+ if (custom) {
851
+ const abs = node_path_1.default.resolve(cwd, custom);
852
+ return node_fs_1.default.existsSync(abs) ? abs : null;
853
+ }
854
+ const srcTs = node_path_1.default.join(cwd, 'src/http/server.ts');
855
+ const srcJs = node_path_1.default.join(cwd, 'src/http/server.js');
856
+ const rootTs = node_path_1.default.join(cwd, 'http/server.ts');
857
+ const rootJs = node_path_1.default.join(cwd, 'http/server.js');
858
+ const hasSrc = node_fs_1.default.existsSync(srcTs) || node_fs_1.default.existsSync(srcJs);
859
+ const hasRoot = node_fs_1.default.existsSync(rootTs) || node_fs_1.default.existsSync(rootJs);
860
+ if (hasSrc && hasRoot) {
861
+ console.error('\n\x1b[31m Ambiguous Entry Point Error\x1b[0m\n' +
862
+ 'You have server entry files in both "src/http" and "http" directories.\n' +
863
+ 'Please keep only one of them to prevent unexpected behavior.\n');
864
+ process.exit(1);
865
+ }
866
+ if (node_fs_1.default.existsSync(srcTs))
867
+ return srcTs;
868
+ if (node_fs_1.default.existsSync(srcJs))
869
+ return srcJs;
870
+ if (node_fs_1.default.existsSync(rootTs))
871
+ return rootTs;
872
+ if (node_fs_1.default.existsSync(rootJs))
873
+ return rootJs;
874
+ return null;
875
+ }
876
+ async function importChokidar() {
877
+ try {
878
+ return await Promise.resolve().then(() => __importStar(require('chokidar')));
879
+ }
880
+ catch {
881
+ return null;
882
+ }
883
+ }
884
+ //# sourceMappingURL=dev.js.map