neex
Version:
Neex - Modern Fullstack Framework Built on Express and Next.js. Fast to Start, Easy to Build, Ready to Deploy
464 lines (463 loc) • 18.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DevManager = void 0;
// src/dev-manager.ts - Ultra-fast TypeScript development server like tsx
const child_process_1 = require("child_process");
const chokidar_1 = require("chokidar");
const logger_manager_js_1 = require("./logger-manager.js");
const chalk_1 = __importDefault(require("chalk"));
const figures_1 = __importDefault(require("figures"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const lodash_1 = require("lodash");
const ts = __importStar(require("typescript"));
const crypto_1 = __importDefault(require("crypto"));
class DevManager {
constructor(options) {
this.process = null;
this.watcher = null;
this.isRestarting = false;
this.restartCount = 0;
this.startTime = null;
this.moduleCache = new Map();
this.currentTempFile = null;
this.isShuttingDown = false;
this.options = options;
this.tempDir = path_1.default.join(process.cwd(), '.neex-temp');
this.debouncedRestart = (0, lodash_1.debounce)(this.restart.bind(this), Math.max(options.delay, 100));
this.tsCompilerOptions = this.loadTsConfig();
this.setupTempDir();
}
setupTempDir() {
if (fs_1.default.existsSync(this.tempDir)) {
try {
fs_1.default.rmSync(this.tempDir, { recursive: true, force: true });
}
catch (error) {
// Ignore cleanup errors
}
}
fs_1.default.mkdirSync(this.tempDir, { recursive: true });
}
loadTsConfig() {
const configPath = this.options.tsConfig || 'tsconfig.json';
const defaultOptions = {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
allowJs: true,
strict: false,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
declaration: false,
sourceMap: this.options.sourceMaps,
inlineSourceMap: false,
inlineSources: false,
removeComments: false,
preserveConstEnums: false,
isolatedModules: true, // For faster compilation
};
if (fs_1.default.existsSync(configPath)) {
try {
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (!configFile.error) {
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path_1.default.dirname(configPath));
if (parsedConfig.errors.length === 0) {
return { ...defaultOptions, ...parsedConfig.options };
}
}
}
catch (error) {
// Fall back to defaults
}
}
return defaultOptions;
}
loadEnvFile() {
let count = 0;
const envFile = this.options.envFile;
if (envFile && fs_1.default.existsSync(envFile)) {
try {
const envContent = fs_1.default.readFileSync(envFile, 'utf8');
const lines = envContent.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...values] = trimmed.split('=');
if (key && values.length > 0) {
process.env[key.trim()] = values.join('=').trim().replace(/^["']|["']$/g, '');
count++;
}
}
}
if (!this.options.quiet) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Loaded ${count} env variables from ${envFile}`, 'info');
}
}
catch (error) {
if (this.options.verbose) {
logger_manager_js_1.loggerManager.printLine(`Failed to load ${envFile}: ${error.message}`, 'warn');
}
}
}
}
createHash(content) {
return crypto_1.default.createHash('md5').update(content).digest('hex');
}
extractDependencies(sourceCode, filePath) {
const dependencies = [];
const importRegex = /(?:import|require)\s*(?:\([^)]*\)|[^;]+?from\s+)?['"`]([^'"`]+)['"`]/g;
let match;
while ((match = importRegex.exec(sourceCode)) !== null) {
const importPath = match[1];
if (importPath.startsWith('.')) {
let resolvedPath = path_1.default.resolve(path_1.default.dirname(filePath), importPath);
// Try to resolve with extensions
if (!fs_1.default.existsSync(resolvedPath)) {
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
const withExt = resolvedPath + ext;
if (fs_1.default.existsSync(withExt)) {
resolvedPath = withExt;
break;
}
}
}
// Try index files
if (!fs_1.default.existsSync(resolvedPath)) {
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
const indexPath = path_1.default.join(resolvedPath, 'index' + ext);
if (fs_1.default.existsSync(indexPath)) {
resolvedPath = indexPath;
break;
}
}
}
if (fs_1.default.existsSync(resolvedPath)) {
dependencies.push(resolvedPath);
}
}
}
return dependencies;
}
compileModule(filePath, forceRecompile = false) {
const absolutePath = path_1.default.resolve(filePath);
try {
const sourceCode = fs_1.default.readFileSync(absolutePath, 'utf8');
const hash = this.createHash(sourceCode);
const cached = this.moduleCache.get(absolutePath);
// Check if we can use cached version
if (!forceRecompile && cached && cached.hash === hash) {
return cached;
}
const dependencies = this.extractDependencies(sourceCode, absolutePath);
// Fast transpile without type checking for development
const result = ts.transpileModule(sourceCode, {
compilerOptions: this.tsCompilerOptions,
fileName: absolutePath,
reportDiagnostics: false // Skip diagnostics for speed
});
const moduleInfo = {
code: result.outputText,
map: result.sourceMapText,
hash,
timestamp: Date.now(),
dependencies
};
this.moduleCache.set(absolutePath, moduleInfo);
if (this.options.verbose) {
logger_manager_js_1.loggerManager.printLine(`Compiled ${path_1.default.relative(process.cwd(), filePath)}`, 'info');
}
return moduleInfo;
}
catch (error) {
logger_manager_js_1.loggerManager.printLine(`Compilation error: ${error.message}`, 'error');
throw error;
}
}
invalidateModuleCache(filePath) {
const absolutePath = path_1.default.resolve(filePath);
// Remove the file itself
this.moduleCache.delete(absolutePath);
// Remove any modules that depend on this file
const toRemove = [];
for (const [cachedPath, info] of this.moduleCache.entries()) {
if (info.dependencies.includes(absolutePath)) {
toRemove.push(cachedPath);
}
}
for (const pathToRemove of toRemove) {
this.moduleCache.delete(pathToRemove);
}
if (this.options.verbose && toRemove.length > 0) {
logger_manager_js_1.loggerManager.printLine(`Invalidated ${toRemove.length + 1} modules`, 'info');
}
}
createExecutableFile() {
// Always force recompile the main file
const mainModule = this.compileModule(this.options.file, true);
// Create a unique temp file
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
const tempFile = path_1.default.join(this.tempDir, `main-${timestamp}-${random}.js`);
let code = mainModule.code;
// Add source map support
if (mainModule.map && this.options.sourceMaps) {
const mapFile = tempFile + '.map';
fs_1.default.writeFileSync(mapFile, mainModule.map);
code += `\n//# sourceMappingURL=${path_1.default.basename(mapFile)}`;
}
fs_1.default.writeFileSync(tempFile, code);
// Clean up old temp file
if (this.currentTempFile && fs_1.default.existsSync(this.currentTempFile)) {
try {
fs_1.default.unlinkSync(this.currentTempFile);
const mapFile = this.currentTempFile + '.map';
if (fs_1.default.existsSync(mapFile)) {
fs_1.default.unlinkSync(mapFile);
}
}
catch (error) {
// Ignore cleanup errors
}
}
this.currentTempFile = tempFile;
return tempFile;
}
async getExecuteCommand() {
if (this.options.execCommand) {
const parts = this.options.execCommand.split(' ');
return { command: parts[0], args: parts.slice(1) };
}
const executableFile = this.createExecutableFile();
const args = [...this.options.nodeArgs, executableFile];
// Add Node.js flags
if (this.options.inspect)
args.unshift('--inspect');
if (this.options.inspectBrk)
args.unshift('--inspect-brk');
if (this.options.sourceMaps)
args.unshift('--enable-source-maps');
return { command: 'node', args };
}
clearConsole() {
if (this.options.clearConsole && process.stdout.isTTY) {
process.stdout.write('\x1Bc'); // Clear screen and scrollback
}
}
async startProcess() {
var _a, _b;
if (this.process)
return;
this.loadEnvFile();
try {
const { command, args } = await this.getExecuteCommand();
this.process = (0, child_process_1.spawn)(command, args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: process.env.NODE_ENV || 'development',
FORCE_COLOR: this.options.color ? '1' : '0',
NODE_OPTIONS: '--max-old-space-size=4096', // Prevent memory issues
},
detached: false // Keep attached for better cleanup
});
this.startTime = new Date();
this.restartCount++;
// Handle stdout/stderr
(_a = this.process.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
process.stdout.write(data);
});
(_b = this.process.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
process.stderr.write(data);
});
this.process.on('error', (error) => {
logger_manager_js_1.loggerManager.printLine(`Process error: ${error.message}`, 'error');
});
this.process.on('exit', (code, signal) => {
if (this.process) {
this.process = null;
if (!this.isRestarting && code !== 0) {
const duration = this.startTime ? Date.now() - this.startTime.getTime() : 0;
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red('✖')} Process exited with code ${code} (${duration}ms)`, 'error');
}
}
});
}
catch (error) {
logger_manager_js_1.loggerManager.printLine(`Failed to start: ${error.message}`, 'error');
throw error;
}
}
async stopProcess() {
if (!this.process)
return;
return new Promise((resolve) => {
const proc = this.process;
this.process = null;
const cleanup = () => {
resolve();
};
proc.on('exit', cleanup);
proc.on('error', cleanup);
try {
proc.kill('SIGTERM');
// Force kill after timeout
setTimeout(() => {
if (!proc.killed) {
proc.kill('SIGKILL');
}
}, 1000);
}
catch (error) {
cleanup();
}
});
}
async restart() {
if (this.isRestarting)
return;
this.isRestarting = true;
// Clear console immediately for better UX
this.clearConsole();
if (!this.options.quiet) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow('⟳')} Restarting...`, 'info');
}
// Stop current process
await this.stopProcess();
// Start new process
await this.startProcess();
this.isRestarting = false;
}
setupWatcher() {
const watchPatterns = this.options.watch;
// Optimized ignore patterns
const ignored = [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'.neex-temp/**',
'**/*.log',
'**/*.d.ts',
'**/*.map',
'**/*.tsbuildinfo',
...this.options.ignore
];
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
ignored,
ignoreInitial: true,
followSymlinks: false,
usePolling: false,
atomic: 50, // Very fast detection
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50
}
});
this.watcher.on('change', (filePath) => {
this.invalidateModuleCache(filePath);
if (this.options.verbose) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue('●')} ${path_1.default.relative(process.cwd(), filePath)}`, 'info');
}
this.debouncedRestart();
});
this.watcher.on('add', (filePath) => {
if (this.options.verbose) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green('+')} ${path_1.default.relative(process.cwd(), filePath)}`, 'info');
}
this.debouncedRestart();
});
this.watcher.on('unlink', (filePath) => {
this.invalidateModuleCache(filePath);
if (this.options.verbose) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red('-')} ${path_1.default.relative(process.cwd(), filePath)}`, 'info');
}
this.debouncedRestart();
});
this.watcher.on('error', (error) => {
logger_manager_js_1.loggerManager.printLine(`Watcher error: ${error.message}`, 'error');
});
}
async start() {
if (!fs_1.default.existsSync(this.options.file)) {
throw new Error(`Target file not found: ${this.options.file}`);
}
const ext = path_1.default.extname(this.options.file);
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
throw new Error(`Unsupported file extension: ${ext}`);
}
// Clear any existing cache
this.moduleCache.clear();
this.setupTempDir();
if (!this.options.quiet) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Starting TypeScript development server...`, 'info');
}
this.setupWatcher();
await this.startProcess();
if (!this.options.quiet) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green('✓')} Watching for changes...`, 'info');
}
}
async stop() {
if (this.isShuttingDown)
return;
this.isShuttingDown = true;
if (!this.options.quiet) {
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow('⏹')} Stopping dev server...`, 'info');
}
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
await this.stopProcess();
// Cleanup temp files
if (fs_1.default.existsSync(this.tempDir)) {
try {
fs_1.default.rmSync(this.tempDir, { recursive: true, force: true });
}
catch (error) {
// Ignore cleanup errors
}
}
this.moduleCache.clear();
}
}
exports.DevManager = DevManager;