@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.
- package/README.md +35 -2
- package/bin/exisjs.js +2 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +378 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/clean.d.ts +5 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +30 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/dev.d.ts +28 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +884 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +228 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/exports.d.ts +2 -0
- package/dist/commands/exports.d.ts.map +1 -0
- package/dist/commands/exports.js +48 -0
- package/dist/commands/exports.js.map +1 -0
- package/dist/commands/generate.d.ts +33 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +469 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +37 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +19 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +116 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/repl.d.ts +6 -0
- package/dist/commands/repl.d.ts.map +1 -0
- package/dist/commands/repl.js +75 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/commands/run.d.ts +7 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +101 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +8 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/test.d.ts +8 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +166 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +276 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/inspect-routes.d.ts +2 -0
- package/dist/lib/inspect-routes.d.ts.map +1 -0
- package/dist/lib/inspect-routes.js +150 -0
- package/dist/lib/inspect-routes.js.map +1 -0
- package/dist/lib/start-repl.d.ts +2 -0
- package/dist/lib/start-repl.d.ts.map +1 -0
- package/dist/lib/start-repl.js +120 -0
- package/dist/lib/start-repl.js.map +1 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +489 -0
- package/dist/manifest.js.map +1 -0
- package/dist/routes.d.ts +17 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +316 -0
- package/dist/routes.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +54 -0
- package/dist/utils.js.map +1 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>')}</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
|