refactor cli

This commit is contained in:
Michał Sieciechowicz 2026-01-17 00:25:26 +01:00
parent be5961dde9
commit 0fe8b0fdd2
9 changed files with 1494 additions and 1339 deletions

View File

@ -9,24 +9,45 @@ const args = process.argv.slice(3);
if (!command) {
console.log('Usage: qu <command> [options]');
console.log('\nAvailable commands:');
console.log(' build Build the application');
console.log(' build [options] Build the application');
console.log(' serve [options] Watch and rebuild on file changes');
console.log(' --port, -p Specify port (default: 4300)');
console.log(' help Show this help message');
console.log('\nGlobal options:');
console.log(' -c, --configuration <env> Specify environment (development/production)');
console.log(' -e, --environment <env> Alias for --configuration');
console.log('\nBuild options:');
console.log(' -v, --verbose Show detailed build logs');
console.log('\nServe options:');
console.log(' -p, --port <port> Specify port (default: 4200)');
console.log(' -v, --verbose Show detailed server logs');
console.log('\nOther commands:');
console.log(' help Show this help message');
process.exit(0);
}
if (command === 'help' || command === '--help' || command === '-h') {
console.log('Usage: qu <command> [options]');
console.log('\nAvailable commands:');
console.log(' build Build the application');
console.log(' build [options] Build the application');
console.log(' serve [options] Watch and rebuild on file changes');
console.log(' --port, -p Specify port (default: 4300)');
console.log(' help Show this help message');
console.log('\nGlobal options:');
console.log(' -c, --configuration <env> Specify environment (development/production)');
console.log(' -e, --environment <env> Alias for --configuration');
console.log('\nBuild options:');
console.log(' -v, --verbose Show detailed build logs');
console.log('\nServe options:');
console.log(' -p, --port <port> Specify port (default: 4200)');
console.log(' -v, --verbose Show detailed server logs');
console.log('\nOther commands:');
console.log(' help Show this help message');
console.log('\nExamples:');
console.log(' qu build');
console.log(' qu build -c production');
console.log(' qu build -v');
console.log(' qu build -c production --verbose');
console.log(' qu serve');
console.log(' qu serve --port 3000');
console.log(' qu serve -p 8080');
console.log(' qu serve -v');
console.log(' qu serve -c development --port 3000');
console.log(' qu serve -p 8080 --verbose');
process.exit(0);
}
@ -49,7 +70,7 @@ if (command === 'build') {
try {
const cwd = process.cwd();
const cliPath = findQuarcCliPath(cwd);
const buildScript = path.join(cliPath, 'build.ts');
const buildScript = path.join(cliPath, 'scripts', 'build.ts');
const tsNodePath = path.join(cwd, 'node_modules', '.bin', 'ts-node');
const buildArgs = args.join(' ');
execSync(`${tsNodePath} ${buildScript} ${buildArgs}`, { stdio: 'inherit', cwd });
@ -60,9 +81,10 @@ if (command === 'build') {
try {
const cwd = process.cwd();
const cliPath = findQuarcCliPath(cwd);
const serveScript = path.join(cliPath, 'serve.ts');
const serveScript = path.join(cliPath, 'scripts', 'serve.ts');
const tsNodePath = path.join(cwd, 'node_modules', '.bin', 'ts-node');
execSync(`${tsNodePath} ${serveScript}`, { stdio: 'inherit', cwd });
const serveArgs = args.join(' ');
execSync(`${tsNodePath} ${serveScript} ${serveArgs}`, { stdio: 'inherit', cwd });
} catch (error) {
process.exit(1);
}

View File

@ -1,710 +0,0 @@
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { execSync } from 'child_process';
import * as esbuild from 'esbuild';
import { minify } from 'terser';
import Table from 'cli-table3';
import * as sass from 'sass';
import { liteTransformer } from './lite-transformer';
import { consoleTransformer } from './build/transformers/console-transformer';
const projectRoot = process.cwd();
const srcDir = path.join(projectRoot, 'src');
const publicDir = path.join(srcDir, 'public');
const distDir = path.join(projectRoot, 'dist');
const configPath = path.join(projectRoot, 'quarc.json');
interface SizeThreshold {
warning: string;
error: string;
}
interface EnvironmentConfig {
treatWarningsAsErrors: boolean;
minifyNames: boolean;
generateSourceMaps: boolean;
compressed?: boolean;
}
interface ActionsConfig {
prebuild?: string[];
postbuild?: string[];
}
interface LiteConfig {
environment: string;
build: {
actions?: ActionsConfig;
minifyNames: boolean;
scripts?: string[];
externalEntryPoints?: string[];
styles?: string[];
externalStyles?: string[];
limits: {
total: SizeThreshold;
main: SizeThreshold;
sourceMaps: SizeThreshold;
components?: SizeThreshold;
};
};
environments: {
[key: string]: EnvironmentConfig;
};
}
interface ValidationResult {
status: 'success' | 'warning' | 'error';
message: string;
actual: number;
limit: number;
}
function parseSizeString(sizeStr: string): number {
const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB)$/i);
if (!match) throw new Error(`Invalid size format: ${sizeStr}`);
const value = parseFloat(match[1]);
const unit = match[2].toUpperCase();
const multipliers: { [key: string]: number } = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
return value * (multipliers[unit] || 1);
}
function getCliEnvironment(): string | undefined {
const args = process.argv.slice(2);
const envIndex = args.findIndex(arg => arg === '--environment' || arg === '-e');
if (envIndex !== -1 && args[envIndex + 1]) {
return args[envIndex + 1];
}
return undefined;
}
function loadConfig(): LiteConfig {
const cliEnv = getCliEnvironment();
if (!fs.existsSync(configPath)) {
return {
environment: cliEnv ?? 'development',
build: {
minifyNames: false,
limits: {
total: { warning: '50 KB', error: '60 KB' },
main: { warning: '15 KB', error: '20 KB' },
sourceMaps: { warning: '10 KB', error: '20 KB' },
},
},
environments: {
development: {
treatWarningsAsErrors: false,
minifyNames: false,
generateSourceMaps: true,
},
production: {
treatWarningsAsErrors: true,
minifyNames: true,
generateSourceMaps: false,
},
},
};
}
const content = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(content) as LiteConfig;
if (cliEnv) {
config.environment = cliEnv;
}
return config;
}
function getEnvironmentConfig(config: LiteConfig): EnvironmentConfig {
const envConfig = config.environments[config.environment];
if (!envConfig) {
console.warn(`Environment '${config.environment}' not found in config, using defaults`);
return {
treatWarningsAsErrors: false,
minifyNames: false,
generateSourceMaps: true,
};
}
return envConfig;
}
function ensureDirectoryExists(dir: string): void {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function copyDirectory(src: string, dest: string): void {
if (!fs.existsSync(src)) {
console.warn(`Source directory not found: ${src}`);
return;
}
ensureDirectoryExists(dest);
const files = fs.readdirSync(src);
files.forEach(file => {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const stat = fs.statSync(srcPath);
if (stat.isDirectory()) {
copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
});
}
async function bundleTypeScript(): Promise<void> {
try {
console.log('Bundling TypeScript with esbuild...');
const config = loadConfig();
const envConfig = getEnvironmentConfig(config);
const mainTsPath = path.join(srcDir, 'main.ts');
await esbuild.build({
entryPoints: [mainTsPath],
bundle: true,
minify: false,
sourcemap: envConfig.generateSourceMaps,
outdir: distDir,
format: 'esm',
target: 'ES2020',
splitting: true,
chunkNames: 'chunks/[name]-[hash]',
external: [],
plugins: [liteTransformer(), consoleTransformer()],
tsconfig: path.join(projectRoot, 'tsconfig.json'),
treeShaking: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': config.environment === 'production' ? '"production"' : '"development"',
},
drop: config.environment === 'production' ? ['console', 'debugger'] : ['debugger'],
pure: config.environment === 'production' ? ['console.log', 'console.error', 'console.warn', 'console.info', 'console.debug'] : [],
globalName: undefined,
});
console.log('TypeScript bundling completed.');
await bundleExternalEntryPoints();
await obfuscateAndMinifyBundles();
} catch (error) {
console.error('TypeScript bundling failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
async function bundleExternalEntryPoints(): Promise<void> {
const config = loadConfig();
const envConfig = getEnvironmentConfig(config);
const externalEntryPoints = config.build.externalEntryPoints || [];
if (externalEntryPoints.length === 0) {
return;
}
console.log('Bundling external entry points...');
const externalDistDir = path.join(distDir, 'external');
ensureDirectoryExists(externalDistDir);
for (const entryPoint of externalEntryPoints) {
const entryPath = path.join(projectRoot, entryPoint);
if (!fs.existsSync(entryPath)) {
console.warn(`External entry point not found: ${entryPath}`);
continue;
}
const basename = path.basename(entryPoint, '.ts');
await esbuild.build({
entryPoints: [entryPath],
bundle: true,
minify: false,
sourcemap: envConfig.generateSourceMaps,
outfile: path.join(externalDistDir, `${basename}.js`),
format: 'esm',
target: 'ES2020',
splitting: false,
external: [],
plugins: [liteTransformer(), consoleTransformer()],
tsconfig: path.join(projectRoot, 'tsconfig.json'),
treeShaking: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': config.environment === 'production' ? '"production"' : '"development"',
},
drop: config.environment === 'production' ? ['console', 'debugger'] : ['debugger'],
pure: config.environment === 'production' ? ['console.log', 'console.error', 'console.warn', 'console.info', 'console.debug'] : [],
});
console.log(`✓ Bundled external: ${basename}.js`);
}
}
async function obfuscateAndMinifyBundles(): Promise<void> {
try {
console.log('Applying advanced obfuscation and minification...');
const config = loadConfig();
const envConfig = getEnvironmentConfig(config);
const collectJsFiles = (dir: string, prefix = ''): { file: string; filePath: string }[] => {
const results: { file: string; filePath: string }[] = [];
if (!fs.existsSync(dir)) return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
results.push(...collectJsFiles(fullPath, relativePath));
} else if (entry.name.endsWith('.js') && !entry.name.endsWith('.map')) {
results.push({ file: relativePath, filePath: fullPath });
}
}
return results;
};
const jsFiles = collectJsFiles(distDir);
for (const { file, filePath } of jsFiles) {
const code = fs.readFileSync(filePath, 'utf-8');
const result = await minify(code, {
compress: {
passes: 3,
unsafe: true,
unsafe_methods: true,
unsafe_proto: true,
drop_console: config.environment === 'production',
drop_debugger: true,
inline: 3,
reduce_vars: true,
reduce_funcs: true,
collapse_vars: true,
dead_code: true,
evaluate: true,
hoist_funs: true,
hoist_vars: true,
if_return: true,
join_vars: true,
loops: true,
properties: false,
sequences: true,
side_effects: true,
switches: true,
typeofs: true,
unused: true,
},
mangle: envConfig.minifyNames ? {
toplevel: true,
keep_classnames: false,
keep_fnames: false,
properties: false,
} : false,
output: {
comments: false,
beautify: false,
max_line_len: 1000,
},
});
if (result.code) {
fs.writeFileSync(filePath, result.code, 'utf-8');
const originalSize = code.length;
const newSize = result.code.length;
const reduction = ((1 - newSize / originalSize) * 100).toFixed(2);
console.log(`${file}: ${originalSize}${newSize} bytes (${reduction}% reduction)`);
}
}
console.log('Obfuscation and minification completed.');
} catch (error) {
console.error('Obfuscation failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
async function compileStyleFile(stylePath: string, outputDir: string): Promise<void> {
const fullPath = path.join(projectRoot, stylePath);
if (!fs.existsSync(fullPath)) {
console.warn(`Style file not found: ${fullPath}`);
return;
}
const ext = path.extname(fullPath);
const basename = path.basename(fullPath, ext);
const outputPath = path.join(outputDir, `${basename}.css`);
ensureDirectoryExists(outputDir);
if (ext === '.scss' || ext === '.sass') {
try {
const result = sass.compile(fullPath, {
style: 'compressed',
sourceMap: false,
});
fs.writeFileSync(outputPath, result.css, 'utf-8');
console.log(`✓ Compiled ${stylePath}${basename}.css`);
} catch (error) {
console.error(`Failed to compile ${stylePath}:`, error instanceof Error ? error.message : String(error));
throw error;
}
} else if (ext === '.css') {
fs.copyFileSync(fullPath, outputPath);
console.log(`✓ Copied ${stylePath}${basename}.css`);
}
}
async function compileSCSS(): Promise<void> {
const config = loadConfig();
const styles = config.build.styles || [];
const externalStyles = config.build.externalStyles || [];
if (styles.length === 0 && externalStyles.length === 0) {
return;
}
console.log('Compiling SCSS files...');
for (const stylePath of styles) {
await compileStyleFile(stylePath, distDir);
}
const externalDistDir = path.join(distDir, 'external');
for (const stylePath of externalStyles) {
await compileStyleFile(stylePath, externalDistDir);
}
}
function injectScriptsAndStyles(indexPath: string): void {
if (!fs.existsSync(indexPath)) {
console.warn(`Index file not found: ${indexPath}`);
return;
}
const config = loadConfig();
let html = fs.readFileSync(indexPath, 'utf-8');
const styles = config.build.styles || [];
const scripts = config.build.scripts || [];
let styleInjections = '';
for (const stylePath of styles) {
const basename = path.basename(stylePath, path.extname(stylePath));
const cssFile = `${basename}.css`;
if (!html.includes(cssFile)) {
styleInjections += ` <link rel="stylesheet" href="./${cssFile}">\n`;
}
}
if (styleInjections) {
html = html.replace('</head>', `${styleInjections}</head>`);
}
let scriptInjections = '';
for (const scriptPath of scripts) {
const basename = path.basename(scriptPath);
if (!html.includes(basename)) {
scriptInjections += ` <script type="module" src="./${basename}"></script>\n`;
}
}
const mainScript = ` <script type="module" src="./main.js"></script>\n`;
if (!html.includes('main.js')) {
scriptInjections += mainScript;
}
if (scriptInjections) {
html = html.replace('</body>', `${scriptInjections}</body>`);
}
fs.writeFileSync(indexPath, html, 'utf-8');
console.log('Injected scripts and styles into index.html');
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getGzipSize(content: Buffer | string): number {
const buffer = typeof content === 'string' ? Buffer.from(content) : content;
return zlib.gzipSync(buffer, { level: 9 }).length;
}
function displayBuildStats(): void {
const files: { name: string; size: number; gzipSize: number; path: string }[] = [];
let totalSize = 0;
let totalGzipSize = 0;
let mainSize = 0;
let mapSize = 0;
let externalSize = 0;
const config = loadConfig();
const envConfig = getEnvironmentConfig(config);
const showGzip = envConfig.compressed ?? false;
const isExternalFile = (relativePath: string): boolean => relativePath.startsWith('external/');
const isMapFile = (name: string): boolean => name.endsWith('.map');
const walkDir = (dir: string, prefix = ''): void => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkDir(fullPath, relativePath);
} else if (!entry.name.endsWith('.gz')) {
const content = fs.readFileSync(fullPath);
const size = content.length;
const gzipSize = getGzipSize(content);
files.push({ name: entry.name, size, gzipSize, path: relativePath });
if (isMapFile(entry.name)) {
mapSize += size;
} else if (isExternalFile(relativePath)) {
externalSize += size;
} else {
totalSize += size;
totalGzipSize += gzipSize;
}
if (entry.name === 'main.js') mainSize = size;
}
});
};
walkDir(distDir);
files.sort((a, b) => b.size - a.size);
const validationResults: { [key: string]: ValidationResult } = {};
// Validate limits
const totalWarning = parseSizeString(config.build.limits.total.warning);
const totalError = parseSizeString(config.build.limits.total.error);
const mainWarning = parseSizeString(config.build.limits.main.warning);
const mainError = parseSizeString(config.build.limits.main.error);
const mapWarning = parseSizeString(config.build.limits.sourceMaps.warning);
const mapError = parseSizeString(config.build.limits.sourceMaps.error);
validationResults.total = validateSizeWithThresholds('Total Size', totalSize, totalWarning, totalError);
validationResults.main = validateSizeWithThresholds('Main Bundle', mainSize, mainWarning, mainError);
validationResults.sourceMaps = validateSizeWithThresholds('Source Maps', mapSize, mapWarning, mapError);
console.log(`\n📊 Size breakdown:`);
console.log(` App total: ${formatBytes(totalSize)}`);
console.log(` External: ${formatBytes(externalSize)}`);
console.log(` Maps: ${formatBytes(mapSize)}`);
// Display table
const tableHead = showGzip
? ['📄 File', '💾 Size', '📦 Gzip', '✓ Status']
: ['📄 File', '💾 Size', '✓ Status'];
const colWidths = showGzip ? [32, 12, 12, 10] : [40, 15, 12];
const table = new Table({
head: tableHead,
style: { head: [], border: ['cyan'] },
wordWrap: true,
colWidths,
});
files.forEach(file => {
const sizeStr = formatBytes(file.size);
const gzipStr = formatBytes(file.gzipSize);
const fileName = file.path.length > 28 ? file.path.substring(0, 25) + '...' : file.path;
if (showGzip) {
table.push([fileName, sizeStr, gzipStr, '✓']);
} else {
table.push([fileName, sizeStr, '✓']);
}
});
if (showGzip) {
table.push([
'\x1b[1mTOTAL\x1b[0m',
'\x1b[1m' + formatBytes(totalSize) + '\x1b[0m',
'\x1b[1m' + formatBytes(totalGzipSize) + '\x1b[0m',
'\x1b[1m✓\x1b[0m',
]);
} else {
table.push(['\x1b[1mTOTAL\x1b[0m', '\x1b[1m' + formatBytes(totalSize) + '\x1b[0m', '\x1b[1m✓\x1b[0m']);
}
console.log('\n' + table.toString());
// Display validation results
// Check if build should fail
const hasErrors = Object.values(validationResults).some(r => r.status === 'error');
const hasWarnings = Object.values(validationResults).some(r => r.status === 'warning');
const treatWarningsAsErrors = envConfig.treatWarningsAsErrors;
if (hasErrors || (hasWarnings && treatWarningsAsErrors)) {
console.error('\n❌ Build validation failed!');
Object.entries(validationResults).forEach(([key, result]) => {
if (result.status === 'error' || (result.status === 'warning' && treatWarningsAsErrors)) {
console.error(` ${result.message}`);
}
});
process.exit(1);
}
if (hasWarnings) {
console.warn('\n⚠ Build completed with warnings:');
Object.entries(validationResults).forEach(([key, result]) => {
if (result.status === 'warning') {
console.warn(` ${result.message}`);
}
});
}
}
function validateSizeWithThresholds(name: string, actual: number, warningLimit: number, errorLimit: number): ValidationResult {
if (actual > errorLimit) {
return {
status: 'error',
message: `${name}: ${formatBytes(actual)} exceeds error limit of ${formatBytes(errorLimit)}`,
actual,
limit: warningLimit,
};
}
if (actual > warningLimit) {
return {
status: 'warning',
message: `${name}: ${formatBytes(actual)} exceeds warning limit of ${formatBytes(warningLimit)}`,
actual,
limit: warningLimit,
};
}
return {
status: 'success',
message: `${name}: ${formatBytes(actual)} is within limits`,
actual,
limit: warningLimit,
};
}
function generateCompressedFiles(): void {
const config = loadConfig();
const envConfig = getEnvironmentConfig(config);
if (!envConfig.compressed) {
return;
}
console.log('Generating compressed files...');
const compressibleExtensions = ['.js', '.css', '.html', '.json', '.svg', '.xml'];
const walkAndCompress = (dir: string): void => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkAndCompress(fullPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (compressibleExtensions.includes(ext)) {
const content = fs.readFileSync(fullPath);
const compressed = zlib.gzipSync(content, { level: 9 });
const gzPath = fullPath + '.gz';
fs.writeFileSync(gzPath, compressed);
}
}
});
};
walkAndCompress(distDir);
console.log('✓ Compressed files generated (.gz)');
}
function runBuildActions(phase: 'prebuild' | 'postbuild'): void {
const config = loadConfig();
const actions = config.build.actions?.[phase] || [];
if (actions.length === 0) return;
console.log(`🔧 Running ${phase} actions...`);
for (const action of actions) {
console.log(`${action}`);
try {
execSync(action, {
cwd: projectRoot,
stdio: 'inherit',
});
} catch (err) {
console.error(` ❌ Action failed: ${action}`);
throw err;
}
}
}
async function build(): Promise<void> {
try {
console.log('Starting build process...');
runBuildActions('prebuild');
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true });
}
ensureDirectoryExists(distDir);
console.log('Copying public files...');
copyDirectory(publicDir, distDir);
await compileSCSS();
await bundleTypeScript();
const indexPath = path.join(distDir, 'index.html');
injectScriptsAndStyles(indexPath);
generateCompressedFiles();
displayBuildStats();
runBuildActions('postbuild');
console.log('Build completed successfully!');
console.log(`Output directory: ${distDir}`);
} catch (error) {
console.error('Build failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
build().catch(error => {
console.error('Build process failed:', error);
process.exit(1);
});

123
cli/quarc-transformer.ts Normal file
View File

@ -0,0 +1,123 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as path from 'path';
import { BaseProcessor } from './processors/base-processor';
import { TemplateProcessor } from './processors/template-processor';
import { StyleProcessor } from './processors/style-processor';
import { DIProcessor } from './processors/di-processor';
import { ClassDecoratorProcessor } from './processors/class-decorator-processor';
import { SignalTransformerProcessor, SignalTransformerError } from './processors/signal-transformer-processor';
import { DirectiveCollectorProcessor } from './processors/directive-collector-processor';
export class BuildError extends Error {
constructor(
message: string,
public filePath: string,
public processorName: string,
public originalError?: Error,
) {
super(message);
this.name = 'BuildError';
}
}
export class QuarcTransformer {
private processors: BaseProcessor[];
constructor(processors?: BaseProcessor[]) {
this.processors = processors || [
new ClassDecoratorProcessor(),
new SignalTransformerProcessor(),
new TemplateProcessor(),
new StyleProcessor(),
new DIProcessor(),
new DirectiveCollectorProcessor(),
];
}
createPlugin(): esbuild.Plugin {
return {
name: 'quarc-transformer',
setup: (build) => {
build.onLoad({ filter: /\.(ts)$/ }, async (args) => {
if (args.path.includes('node_modules')) {
return {
contents: await fs.promises.readFile(args.path, 'utf8'),
loader: 'ts',
};
}
const source = await fs.promises.readFile(args.path, 'utf8');
const fileDir = path.dirname(args.path);
const skipPaths = [
'/quarc/core/module/',
'/quarc/core/angular/',
'/quarc/router/angular/',
];
if (skipPaths.some(p => args.path.includes(p))) {
return {
contents: source,
loader: 'ts',
};
}
let currentSource = source;
for (const processor of this.processors) {
try {
const result = await processor.process({
filePath: args.path,
fileDir,
source: currentSource,
});
if (result.modified) {
currentSource = result.source;
}
} catch (error) {
if (error instanceof SignalTransformerError) {
return {
errors: [{
text: error.message,
location: {
file: args.path,
namespace: 'file',
},
}],
};
}
const buildError = new BuildError(
error instanceof Error ? error.message : String(error),
args.path,
processor.name,
error instanceof Error ? error : undefined,
);
return {
errors: [{
text: `[${processor.name}] ${buildError.message}`,
location: {
file: args.path,
namespace: 'file',
},
}],
};
}
}
return {
contents: currentSource,
loader: 'ts',
};
});
},
};
}
}
export function quarcTransformer(processors?: BaseProcessor[]): esbuild.Plugin {
const transformer = new QuarcTransformer(processors);
return transformer.createPlugin();
}

635
cli/scripts/base-builder.ts Normal file
View File

@ -0,0 +1,635 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { execSync } from 'child_process';
import * as esbuild from 'esbuild';
import { minify } from 'terser';
import Table from 'cli-table3';
import * as sass from 'sass';
import { quarcTransformer } from '../quarc-transformer';
import { consoleTransformer } from '../build/transformers/console-transformer';
import {
QuarcConfig,
EnvironmentConfig,
ValidationResult,
BuildConfig,
} from '../types';
export abstract class BaseBuilder {
protected projectRoot: string;
protected srcDir: string;
protected publicDir: string;
protected distDir: string;
protected configPath: string;
protected config: QuarcConfig;
protected envConfig: EnvironmentConfig;
constructor() {
this.projectRoot = process.cwd();
this.srcDir = path.join(this.projectRoot, 'src');
this.publicDir = path.join(this.srcDir, 'public');
this.distDir = path.join(this.projectRoot, 'dist');
this.configPath = path.join(this.projectRoot, 'quarc.json');
this.config = this.loadConfig();
this.envConfig = this.getEnvironmentConfig();
}
protected isVerbose(): boolean {
const args = process.argv.slice(2);
return args.includes('-v') || args.includes('--verbose');
}
protected getCliConfiguration(): string | undefined {
const args = process.argv.slice(2);
const configIndex = args.findIndex(arg => arg === '--configuration' || arg === '-c');
if (configIndex !== -1 && args[configIndex + 1]) {
return args[configIndex + 1];
}
const envIndex = args.findIndex(arg => arg === '--environment' || arg === '-e');
if (envIndex !== -1 && args[envIndex + 1]) {
return args[envIndex + 1];
}
return undefined;
}
protected loadConfig(): QuarcConfig {
const cliConfig = this.getCliConfiguration();
if (!fs.existsSync(this.configPath)) {
return {
environment: cliConfig ?? 'development',
build: {
minifyNames: false,
limits: {
total: { warning: '50 KB', error: '60 KB' },
main: { warning: '15 KB', error: '20 KB' },
sourceMaps: { warning: '10 KB', error: '20 KB' },
},
},
environments: {
development: {
treatWarningsAsErrors: false,
minifyNames: false,
generateSourceMaps: true,
},
production: {
treatWarningsAsErrors: true,
minifyNames: true,
generateSourceMaps: false,
},
},
};
}
const content = fs.readFileSync(this.configPath, 'utf-8');
const config = JSON.parse(content) as QuarcConfig;
if (cliConfig) {
config.environment = cliConfig;
}
return config;
}
protected getEnvironmentConfig(): EnvironmentConfig {
const envConfig = this.config.environments[this.config.environment];
if (!envConfig) {
console.warn(`Environment '${this.config.environment}' not found in config, using defaults`);
return {
treatWarningsAsErrors: false,
minifyNames: false,
generateSourceMaps: true,
};
}
return envConfig;
}
protected ensureDirectoryExists(dir: string): void {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
protected copyDirectory(src: string, dest: string): void {
if (!fs.existsSync(src)) {
console.warn(`Source directory not found: ${src}`);
return;
}
this.ensureDirectoryExists(dest);
const files = fs.readdirSync(src);
files.forEach(file => {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const stat = fs.statSync(srcPath);
if (stat.isDirectory()) {
this.copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
});
}
protected async bundleTypeScript(): Promise<void> {
try {
if (this.isVerbose()) console.log('Bundling TypeScript with esbuild...');
const mainTsPath = path.join(this.srcDir, 'main.ts');
await esbuild.build({
entryPoints: [mainTsPath],
bundle: true,
minify: false,
sourcemap: this.envConfig.generateSourceMaps,
outdir: this.distDir,
format: 'esm',
target: 'ES2020',
splitting: true,
chunkNames: 'chunks/[name]-[hash]',
external: [],
plugins: [quarcTransformer(), consoleTransformer()],
tsconfig: path.join(this.projectRoot, 'tsconfig.json'),
treeShaking: true,
logLevel: this.isVerbose() ? 'info' : 'silent',
define: {
'process.env.NODE_ENV': this.config.environment === 'production' ? '"production"' : '"development"',
},
drop: this.config.environment === 'production' ? ['console', 'debugger'] : ['debugger'],
pure: this.config.environment === 'production' ? ['console.log', 'console.error', 'console.warn', 'console.info', 'console.debug'] : [],
globalName: undefined,
});
if (this.isVerbose()) console.log('TypeScript bundling completed.');
await this.bundleExternalEntryPoints();
await this.obfuscateAndMinifyBundles();
} catch (error) {
console.error('TypeScript bundling failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
protected async bundleExternalEntryPoints(): Promise<void> {
const externalEntryPoints = this.config.build?.externalEntryPoints || [];
if (externalEntryPoints.length === 0) {
return;
}
if (this.isVerbose()) console.log('Bundling external entry points...');
const externalDistDir = path.join(this.distDir, 'external');
this.ensureDirectoryExists(externalDistDir);
for (const entryPoint of externalEntryPoints) {
const entryPath = path.join(this.projectRoot, entryPoint);
if (!fs.existsSync(entryPath)) {
console.warn(`External entry point not found: ${entryPath}`);
continue;
}
const basename = path.basename(entryPoint, '.ts');
await esbuild.build({
entryPoints: [entryPath],
bundle: true,
minify: false,
sourcemap: this.envConfig.generateSourceMaps,
outfile: path.join(externalDistDir, `${basename}.js`),
format: 'esm',
target: 'ES2020',
splitting: false,
external: [],
plugins: [quarcTransformer(), consoleTransformer()],
tsconfig: path.join(this.projectRoot, 'tsconfig.json'),
treeShaking: true,
logLevel: this.isVerbose() ? 'info' : 'silent',
define: {
'process.env.NODE_ENV': this.config.environment === 'production' ? '"production"' : '"development"',
},
drop: this.config.environment === 'production' ? ['console', 'debugger'] : ['debugger'],
pure: this.config.environment === 'production' ? ['console.log', 'console.error', 'console.warn', 'console.info', 'console.debug'] : [],
});
if (this.isVerbose()) console.log(`✓ Bundled external: ${basename}.js`);
}
}
protected async obfuscateAndMinifyBundles(): Promise<void> {
try {
if (this.isVerbose()) console.log('Applying advanced obfuscation and minification...');
const collectJsFiles = (dir: string, prefix = ''): { file: string; filePath: string }[] => {
const results: { file: string; filePath: string }[] = [];
if (!fs.existsSync(dir)) return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
results.push(...collectJsFiles(fullPath, relativePath));
} else if (entry.name.endsWith('.js') && !entry.name.endsWith('.map')) {
results.push({ file: relativePath, filePath: fullPath });
}
}
return results;
};
const jsFiles = collectJsFiles(this.distDir);
for (const { file, filePath } of jsFiles) {
const code = fs.readFileSync(filePath, 'utf-8');
const result = await minify(code, {
compress: {
passes: 3,
unsafe: true,
unsafe_methods: true,
unsafe_proto: true,
drop_console: this.config.environment === 'production',
drop_debugger: true,
inline: 3,
reduce_vars: true,
reduce_funcs: true,
collapse_vars: true,
dead_code: true,
evaluate: true,
hoist_funs: true,
hoist_vars: true,
if_return: true,
join_vars: true,
loops: true,
properties: false,
sequences: true,
side_effects: true,
switches: true,
typeofs: true,
unused: true,
},
mangle: this.envConfig.minifyNames ? {
toplevel: true,
keep_classnames: false,
keep_fnames: false,
properties: false,
} : false,
output: {
comments: false,
beautify: false,
max_line_len: 1000,
},
});
if (result.code) {
fs.writeFileSync(filePath, result.code, 'utf-8');
const originalSize = code.length;
const newSize = result.code.length;
const reduction = ((1 - newSize / originalSize) * 100).toFixed(2);
if (this.isVerbose()) console.log(`${file}: ${originalSize}${newSize} bytes (${reduction}% reduction)`);
}
}
if (this.isVerbose()) console.log('Obfuscation and minification completed.');
} catch (error) {
console.error('Obfuscation failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
protected async compileStyleFile(stylePath: string, outputDir: string): Promise<void> {
const fullPath = path.join(this.projectRoot, stylePath);
if (!fs.existsSync(fullPath)) {
console.warn(`Style file not found: ${fullPath}`);
return;
}
const ext = path.extname(fullPath);
const basename = path.basename(fullPath, ext);
const outputPath = path.join(outputDir, `${basename}.css`);
this.ensureDirectoryExists(outputDir);
if (ext === '.scss' || ext === '.sass') {
try {
const result = sass.compile(fullPath, {
style: 'compressed',
sourceMap: false,
});
fs.writeFileSync(outputPath, result.css, 'utf-8');
if (this.isVerbose()) console.log(`✓ Compiled ${stylePath}${basename}.css`);
} catch (error) {
console.error(`Failed to compile ${stylePath}:`, error instanceof Error ? error.message : String(error));
throw error;
}
} else if (ext === '.css') {
fs.copyFileSync(fullPath, outputPath);
if (this.isVerbose()) console.log(`✓ Copied ${stylePath}${basename}.css`);
}
}
protected async compileSCSS(): Promise<void> {
const styles = this.config.build?.styles || [];
const externalStyles = this.config.build?.externalStyles || [];
if (styles.length === 0 && externalStyles.length === 0) {
return;
}
if (this.isVerbose()) console.log('Compiling SCSS files...');
for (const stylePath of styles) {
await this.compileStyleFile(stylePath, this.distDir);
}
const externalDistDir = path.join(this.distDir, 'external');
for (const stylePath of externalStyles) {
await this.compileStyleFile(stylePath, externalDistDir);
}
}
protected injectScriptsAndStyles(indexPath: string): void {
if (!fs.existsSync(indexPath)) {
console.warn(`Index file not found: ${indexPath}`);
return;
}
let html = fs.readFileSync(indexPath, 'utf-8');
const styles = this.config.build?.styles || [];
const scripts = this.config.build?.scripts || [];
let styleInjections = '';
for (const stylePath of styles) {
const basename = path.basename(stylePath, path.extname(stylePath));
const cssFile = `${basename}.css`;
if (!html.includes(cssFile)) {
styleInjections += ` <link rel="stylesheet" href="./${cssFile}">\n`;
}
}
if (styleInjections) {
html = html.replace('</head>', `${styleInjections}</head>`);
}
let scriptInjections = '';
for (const scriptPath of scripts) {
const basename = path.basename(scriptPath);
if (!html.includes(basename)) {
scriptInjections += ` <script type="module" src="./${basename}"></script>\n`;
}
}
const mainScript = ` <script type="module" src="./main.js"></script>\n`;
if (!html.includes('main.js')) {
scriptInjections += mainScript;
}
if (scriptInjections) {
html = html.replace('</body>', `${scriptInjections}</body>`);
}
fs.writeFileSync(indexPath, html, 'utf-8');
if (this.isVerbose()) console.log('Injected scripts and styles into index.html');
}
protected formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
protected getGzipSize(content: Buffer | string): number {
const buffer = typeof content === 'string' ? Buffer.from(content) : content;
return zlib.gzipSync(buffer, { level: 9 }).length;
}
protected parseSizeString(sizeStr: string): number {
const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB)$/i);
if (!match) throw new Error(`Invalid size format: ${sizeStr}`);
const value = parseFloat(match[1]);
const unit = match[2].toUpperCase();
const multipliers: { [key: string]: number } = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
return value * (multipliers[unit] || 1);
}
protected validateSizeWithThresholds(name: string, actual: number, warningLimit: number, errorLimit: number): ValidationResult {
if (actual > errorLimit) {
return {
status: 'error',
message: `${name}: ${this.formatBytes(actual)} exceeds error limit of ${this.formatBytes(errorLimit)}`,
actual,
limit: warningLimit,
};
}
if (actual > warningLimit) {
return {
status: 'warning',
message: `${name}: ${this.formatBytes(actual)} exceeds warning limit of ${this.formatBytes(warningLimit)}`,
actual,
limit: warningLimit,
};
}
return {
status: 'success',
message: `${name}: ${this.formatBytes(actual)} is within limits`,
actual,
limit: warningLimit,
};
}
protected displayBuildStats(): void {
const files: { name: string; size: number; gzipSize: number; path: string }[] = [];
let totalSize = 0;
let totalGzipSize = 0;
let mainSize = 0;
let mapSize = 0;
let externalSize = 0;
const showGzip = this.envConfig.compressed ?? false;
const isExternalFile = (relativePath: string): boolean => relativePath.startsWith('external/');
const isMapFile = (name: string): boolean => name.endsWith('.map');
const walkDir = (dir: string, prefix = ''): void => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkDir(fullPath, relativePath);
} else if (!entry.name.endsWith('.gz')) {
const content = fs.readFileSync(fullPath);
const size = content.length;
const gzipSize = this.getGzipSize(content);
files.push({ name: entry.name, size, gzipSize, path: relativePath });
if (isMapFile(entry.name)) {
mapSize += size;
} else if (isExternalFile(relativePath)) {
externalSize += size;
} else {
totalSize += size;
totalGzipSize += gzipSize;
}
if (entry.name === 'main.js') mainSize = size;
}
});
};
walkDir(this.distDir);
files.sort((a, b) => b.size - a.size);
const validationResults: { [key: string]: ValidationResult } = {};
const buildConfig = this.config.build as BuildConfig;
const totalWarning = this.parseSizeString(buildConfig.limits.total.warning);
const totalError = this.parseSizeString(buildConfig.limits.total.error);
const mainWarning = this.parseSizeString(buildConfig.limits.main.warning);
const mainError = this.parseSizeString(buildConfig.limits.main.error);
const mapWarning = this.parseSizeString(buildConfig.limits.sourceMaps.warning);
const mapError = this.parseSizeString(buildConfig.limits.sourceMaps.error);
validationResults.total = this.validateSizeWithThresholds('Total Size', totalSize, totalWarning, totalError);
validationResults.main = this.validateSizeWithThresholds('Main Bundle', mainSize, mainWarning, mainError);
validationResults.sourceMaps = this.validateSizeWithThresholds('Source Maps', mapSize, mapWarning, mapError);
console.log(`\n📊 Size breakdown:`);
console.log(` App total: ${this.formatBytes(totalSize)}`);
console.log(` External: ${this.formatBytes(externalSize)}`);
console.log(` Maps: ${this.formatBytes(mapSize)}`);
const tableHead = showGzip
? ['📄 File', '💾 Size', '📦 Gzip', '✓ Status']
: ['📄 File', '💾 Size', '✓ Status'];
const colWidths = showGzip ? [32, 12, 12, 10] : [40, 15, 12];
const table = new Table({
head: tableHead,
style: { head: [], border: ['cyan'] },
wordWrap: true,
colWidths,
});
files.forEach(file => {
const sizeStr = this.formatBytes(file.size);
const gzipStr = this.formatBytes(file.gzipSize);
const fileName = file.path.length > 28 ? file.path.substring(0, 25) + '...' : file.path;
if (showGzip) {
table.push([fileName, sizeStr, gzipStr, '✓']);
} else {
table.push([fileName, sizeStr, '✓']);
}
});
if (showGzip) {
table.push([
'\x1b[1mTOTAL\x1b[0m',
'\x1b[1m' + this.formatBytes(totalSize) + '\x1b[0m',
'\x1b[1m' + this.formatBytes(totalGzipSize) + '\x1b[0m',
'\x1b[1m✓\x1b[0m',
]);
} else {
table.push(['\x1b[1mTOTAL\x1b[0m', '\x1b[1m' + this.formatBytes(totalSize) + '\x1b[0m', '\x1b[1m✓\x1b[0m']);
}
console.log('\n' + table.toString());
const hasErrors = Object.values(validationResults).some(r => r.status === 'error');
const hasWarnings = Object.values(validationResults).some(r => r.status === 'warning');
const treatWarningsAsErrors = this.envConfig.treatWarningsAsErrors;
if (hasErrors || (hasWarnings && treatWarningsAsErrors)) {
console.error('\n❌ Build validation failed!');
Object.entries(validationResults).forEach(([key, result]) => {
if (result.status === 'error' || (result.status === 'warning' && treatWarningsAsErrors)) {
console.error(` ${result.message}`);
}
});
process.exit(1);
}
if (hasWarnings) {
console.warn('\n⚠ Build completed with warnings:');
Object.entries(validationResults).forEach(([key, result]) => {
if (result.status === 'warning') {
console.warn(` ${result.message}`);
}
});
}
}
protected generateCompressedFiles(): void {
if (!this.envConfig.compressed) {
return;
}
if (this.isVerbose()) console.log('Generating compressed files...');
const compressibleExtensions = ['.js', '.css', '.html', '.json', '.svg', '.xml'];
const walkAndCompress = (dir: string): void => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkAndCompress(fullPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (compressibleExtensions.includes(ext)) {
const content = fs.readFileSync(fullPath);
const compressed = zlib.gzipSync(content, { level: 9 });
const gzPath = fullPath + '.gz';
fs.writeFileSync(gzPath, compressed);
}
}
});
};
walkAndCompress(this.distDir);
if (this.isVerbose()) console.log('✓ Compressed files generated (.gz)');
}
protected runBuildActions(phase: 'prebuild' | 'postbuild'): void {
const actions = this.config.build?.actions?.[phase] || [];
if (actions.length === 0) return;
if (this.isVerbose()) console.log(`🔧 Running ${phase} actions...`);
for (const action of actions) {
if (this.isVerbose()) console.log(`${action}`);
try {
execSync(action, {
cwd: this.projectRoot,
stdio: 'inherit',
});
} catch (err) {
console.error(` ❌ Action failed: ${action}`);
throw err;
}
}
}
abstract run(): Promise<void>;
}

47
cli/scripts/build.ts Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env node
import { BaseBuilder } from './base-builder';
class Builder extends BaseBuilder {
async run(): Promise<void> {
try {
if (this.isVerbose()) console.log(`Starting build process (environment: ${this.config.environment})...`);
this.runBuildActions('prebuild');
if (this.distDir && require('fs').existsSync(this.distDir)) {
require('fs').rmSync(this.distDir, { recursive: true, force: true });
}
this.ensureDirectoryExists(this.distDir);
if (this.isVerbose()) console.log('Copying public files...');
this.copyDirectory(this.publicDir, this.distDir);
await this.compileSCSS();
await this.bundleTypeScript();
const indexPath = require('path').join(this.distDir, 'index.html');
this.injectScriptsAndStyles(indexPath);
this.generateCompressedFiles();
this.displayBuildStats();
this.runBuildActions('postbuild');
if (!this.isVerbose()) {
console.log(`\n✅ Build completed | Environment: ${this.config.environment} | Output: ${this.distDir}`);
} else {
console.log('Build completed successfully!');
console.log(`Output directory: ${this.distDir}`);
}
} catch (error) {
console.error('Build failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
}
const builder = new Builder();
builder.run().catch(error => {
console.error('Build process failed:', error);
process.exit(1);
});

557
cli/scripts/serve.ts Normal file
View File

@ -0,0 +1,557 @@
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import { spawn, execSync, ChildProcess } from 'child_process';
import * as http from 'http';
import * as https from 'https';
import { WebSocketServer, WebSocket } from 'ws';
import { BaseBuilder } from './base-builder';
import {
StaticPath,
StaticRemotePath,
} from '../types';
class Server extends BaseBuilder {
private isBuilding = false;
private buildQueued = false;
private wsClients: Set<WebSocket> = new Set();
private httpServer: http.Server | null = null;
private wsServer: WebSocketServer | null = null;
private actionProcesses: ChildProcess[] = [];
private mergedWsConnections: WebSocket[] = [];
private getDevServerPort(): number {
const args = process.argv.slice(2);
const portIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
if (portIndex !== -1 && args[portIndex + 1]) {
const port = parseInt(args[portIndex + 1], 10);
if (!isNaN(port) && port > 0 && port < 65536) {
return port;
}
}
const envConfig = this.config.environments[this.config.environment];
return envConfig?.devServer?.port || 4200;
}
private getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
};
return mimeTypes[ext] || 'application/octet-stream';
}
private getWebSocketConfig() {
const envConfig = this.config.environments[this.config.environment];
return envConfig?.devServer?.websocket;
}
private attachWebSocketServer(server: http.Server): void {
this.wsServer = new WebSocketServer({ server, path: '/qu-ws/' });
this.wsServer.on('connection', (ws: WebSocket) => {
this.wsClients.add(ws);
if (this.isVerbose()) console.log('Client connected to live reload WebSocket');
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
this.handleIncomingMessage(message, ws);
} catch {
}
});
ws.on('close', () => {
this.wsClients.delete(ws);
if (this.isVerbose()) console.log('Client disconnected from live reload WebSocket');
});
ws.send(JSON.stringify({ type: 'connected' }));
});
if (this.isVerbose()) console.log('WebSocket server attached to HTTP server');
this.connectToMergedSources();
}
private connectToMergedSources(): void {
const wsConfig = this.getWebSocketConfig();
const mergeFrom = wsConfig?.mergeFrom || [];
for (const url of mergeFrom) {
this.connectToMergedSource(url);
}
}
private connectToMergedSource(url: string): void {
if (this.isVerbose()) console.log(`Connecting to merged WebSocket source: ${url}`);
const ws = new WebSocket(url);
ws.on('open', () => {
if (this.isVerbose()) console.log(`Connected to merged source: ${url}`);
this.mergedWsConnections.push(ws);
});
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
if (message.type === 'reload') {
this.broadcastToClients(message);
}
} catch {
}
});
ws.on('close', () => {
if (this.isVerbose()) console.log(`Disconnected from merged source: ${url}, reconnecting...`);
this.mergedWsConnections = this.mergedWsConnections.filter(c => c !== ws);
setTimeout(() => this.connectToMergedSource(url), 2000);
});
ws.on('error', (err: Error) => {
console.warn(`WebSocket error for ${url}:`, err.message);
});
}
private handleIncomingMessage(message: { type: string; [key: string]: unknown }, sender: WebSocket): void {
if (message.type === 'reload') {
this.broadcastToClients(message, sender);
this.broadcastToMergedSources(message);
}
}
private broadcastToClients(message: { type: string; [key: string]: unknown }, excludeSender?: WebSocket): void {
const data = JSON.stringify(message);
for (const client of this.wsClients) {
if (client !== excludeSender && client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
}
private broadcastToMergedSources(message: { type: string; [key: string]: unknown }): void {
const data = JSON.stringify(message);
for (const ws of this.mergedWsConnections) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
}
}
private getStaticPaths(): StaticPath[] {
return this.config.serve?.staticPaths || [];
}
private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void {
const parsedUrl = new URL(targetUrl);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const proxyReq = protocol.request(
targetUrl,
{
method: req.method,
headers: {
...req.headers,
host: parsedUrl.host,
},
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
},
);
proxyReq.on('error', (err) => {
console.error('Proxy error:', err.message);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
}
private isRemotePath(staticPath: StaticPath): staticPath is StaticRemotePath {
return 'url' in staticPath;
}
private tryServeStaticPath(reqUrl: string, req: http.IncomingMessage, res: http.ServerResponse): boolean {
const staticPaths = this.getStaticPaths();
for (const staticPath of staticPaths) {
if (reqUrl.startsWith(staticPath.location)) {
const relativePath = reqUrl.slice(staticPath.location.length);
if (this.isRemotePath(staticPath)) {
const targetUrl = staticPath.url + relativePath;
this.proxyRequest(targetUrl, req, res);
return true;
}
const basePath = path.resolve(this.projectRoot, staticPath.path);
let filePath = path.join(basePath, relativePath || 'index.html');
const normalizedFilePath = path.normalize(filePath);
if (!normalizedFilePath.startsWith(basePath)) {
res.writeHead(403);
res.end('Forbidden');
return true;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, 'index.html');
}
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not Found');
return true;
}
const mimeType = this.getMimeType(filePath);
const content = fs.readFileSync(filePath);
res.writeHead(200, {
'Content-Type': mimeType,
'Cache-Control': 'no-cache',
});
res.end(content);
return true;
}
}
return false;
}
private startHttpServer(port: number): void {
this.httpServer = http.createServer((req, res) => {
const reqUrl = req.url || '/';
if (this.tryServeStaticPath(reqUrl, req, res)) {
return;
}
let filePath = path.join(this.distDir, reqUrl === '/' ? 'index.html' : reqUrl);
if (filePath.includes('..')) {
res.writeHead(403);
res.end('Forbidden');
return;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, 'index.html');
}
if (!fs.existsSync(filePath)) {
const indexPath = path.join(this.distDir, 'index.html');
if (fs.existsSync(indexPath)) {
filePath = indexPath;
} else {
res.writeHead(404);
res.end('Not Found');
return;
}
}
const mimeType = this.getMimeType(filePath);
const content = fs.readFileSync(filePath);
res.writeHead(200, {
'Content-Type': mimeType,
'Cache-Control': 'no-cache',
});
res.end(content);
});
this.httpServer.listen(port, () => {
if (!this.isVerbose()) {
console.log(`\n🌐 Server: http://localhost:${port}`);
} else {
console.log(`\n** Quarc Live Development Server is listening on localhost:${port} **`);
console.log(`** Open your browser on http://localhost:${port}/ **\n`);
}
});
this.attachWebSocketServer(this.httpServer);
}
private notifyClients(): void {
const message = { type: 'reload' };
this.broadcastToClients(message);
this.broadcastToMergedSources(message);
if (this.wsClients.size > 0 && this.isVerbose()) {
console.log('📢 Notified clients to reload');
}
}
private async runBuild(): Promise<void> {
if (this.isBuilding) {
this.buildQueued = true;
return;
}
this.isBuilding = true;
this.buildQueued = false;
if (this.isVerbose()) console.log('\n🔨 Building application...');
const startTime = Date.now();
try {
const buildScript = path.join(__dirname, 'build.ts');
const tsNodePath = path.join(this.projectRoot, 'node_modules', '.bin', 'ts-node');
const configArg = ` -c ${this.config.environment}`;
const verboseArg = this.isVerbose() ? ' -v' : '';
execSync(`${tsNodePath} ${buildScript}${configArg}${verboseArg}`, {
stdio: 'inherit',
cwd: this.projectRoot,
});
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
if (this.isVerbose()) console.log(`✅ Build completed in ${duration}s`);
this.notifyClients();
} catch (error) {
console.error('❌ Build failed');
} finally {
this.isBuilding = false;
if (this.buildQueued) {
console.log('⏳ Running queued build...');
setTimeout(() => this.runBuild(), 100);
}
}
}
private watchFiles(): void {
if (this.isVerbose()) console.log(`👀 Watching for changes in ${this.srcDir}...`);
const debounceDelay = 300;
let debounceTimer: NodeJS.Timeout | null = null;
const watcher = fs.watch(this.srcDir, { recursive: true }, (eventType, filename) => {
if (!filename) return;
const ext = path.extname(filename);
if (!['.ts', '.scss', '.sass', '.css', '.html'].includes(ext)) {
return;
}
if (this.isVerbose()) console.log(`📝 File changed: ${filename}`);
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
this.runBuild();
}, debounceDelay);
});
const cleanup = () => {
console.log('\n👋 Stopping watch mode...');
watcher.close();
for (const client of this.wsClients) {
client.close();
}
this.wsClients.clear();
for (const ws of this.mergedWsConnections) {
ws.close();
}
this.mergedWsConnections = [];
if (this.wsServer) {
this.wsServer.close();
}
if (this.httpServer) {
this.httpServer.close();
}
this.terminateActionProcesses();
this.runPostServeActions();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
private injectLiveReloadScript(): void {
const indexPath = path.join(this.distDir, 'index.html');
const wsPort = this.getDevServerPort();
if (!fs.existsSync(indexPath)) {
console.warn('index.html not found in dist directory');
return;
}
let html = fs.readFileSync(indexPath, 'utf-8');
const liveReloadScript = `
<script>
(function() {
let ws;
let reconnectAttempts = 0;
const maxReconnectDelay = 5000;
function connect() {
ws = new WebSocket('ws://localhost:${wsPort}/qu-ws/');
ws.onopen = () => {
console.log('[Live Reload] Connected');
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'reload') {
console.log('[Live Reload] Reloading page...');
window.location.reload();
}
} catch {}
};
ws.onclose = () => {
console.warn('[Live Reload] Connection lost, attempting to reconnect...');
reconnectAttempts++;
const delay = Math.min(1000 * reconnectAttempts, maxReconnectDelay);
setTimeout(connect, delay);
};
ws.onerror = () => {
ws.close();
};
}
connect();
})();
</script>
`;
if (!html.includes('Live Reload')) {
html = html.replace('</body>', `${liveReloadScript}</body>`);
fs.writeFileSync(indexPath, html, 'utf-8');
if (this.isVerbose()) console.log('✅ Injected live reload script into index.html');
}
}
private runPreServeActions(): void {
const actions = this.config.serve?.actions?.preserve || [];
if (actions.length === 0) return;
if (this.isVerbose()) console.log('🔧 Running preserve actions...');
for (const action of actions) {
if (this.isVerbose()) console.log(`${action}`);
const child = spawn(action, [], {
shell: true,
cwd: this.projectRoot,
stdio: 'inherit',
detached: true,
});
this.actionProcesses.push(child);
child.on('error', (err) => {
console.error(` ❌ Action failed: ${action}`, err.message);
});
}
}
private runPostServeActions(): void {
const actions = this.config.serve?.actions?.postserve || [];
if (actions.length === 0) return;
if (this.isVerbose()) console.log('🔧 Running postserve actions...');
for (const action of actions) {
if (this.isVerbose()) console.log(`${action}`);
try {
execSync(action, {
cwd: this.projectRoot,
stdio: 'inherit',
});
} catch (err) {
console.error(` ❌ Action failed: ${action}`);
}
}
}
private terminateActionProcesses(): void {
if (this.actionProcesses.length === 0) return;
if (this.isVerbose()) console.log('🛑 Terminating action processes...');
for (const child of this.actionProcesses) {
if (child.pid && !child.killed) {
try {
process.kill(-child.pid, 'SIGTERM');
if (this.isVerbose()) console.log(` ✓ Terminated process group ${child.pid}`);
} catch (err) {
try {
child.kill('SIGTERM');
if (this.isVerbose()) console.log(` ✓ Terminated process ${child.pid}`);
} catch {
console.warn(` ⚠ Could not terminate process ${child.pid}`);
}
}
}
}
this.actionProcesses = [];
}
async run(): Promise<void> {
const port = this.getDevServerPort();
if (this.isVerbose()) {
console.log('🚀 Starting development server...\n');
console.log(`Environment: ${this.config.environment}`);
}
this.runPreServeActions();
if (this.isVerbose()) console.log('📦 Running initial build...');
await this.runBuild();
this.injectLiveReloadScript();
this.startHttpServer(port);
if (this.isVerbose()) {
console.log('✨ Development server is ready!');
console.log('📂 Serving files from:', this.distDir);
console.log('🔄 Live reload WebSocket enabled on port', port);
console.log('\nPress Ctrl+C to stop\n');
}
this.watchFiles();
}
}
const server = new Server();
server.run().catch(error => {
console.error('Serve process failed:', error);
process.exit(1);
});

View File

@ -1,618 +0,0 @@
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import { spawn, execSync, ChildProcess } from 'child_process';
import * as http from 'http';
import * as https from 'https';
import { WebSocketServer, WebSocket } from 'ws';
const projectRoot = process.cwd();
const srcDir = path.join(projectRoot, 'src');
const distDir = path.join(projectRoot, 'dist');
const configPath = path.join(projectRoot, 'quarc.json');
let isBuilding = false;
let buildQueued = false;
let wsClients: Set<WebSocket> = new Set();
let httpServer: http.Server | null = null;
let wsServer: WebSocketServer | null = null;
let actionProcesses: ChildProcess[] = [];
let mergedWsConnections: WebSocket[] = [];
interface DevServerConfig {
port: number;
websocket?: WebSocketConfig;
}
interface WebSocketConfig {
mergeFrom?: string[];
}
interface StaticLocalPath {
location: string;
path: string;
}
interface StaticRemotePath {
location: string;
url: string;
}
type StaticPath = StaticLocalPath | StaticRemotePath;
interface ActionsConfig {
preserve?: string[];
postserve?: string[];
}
interface ServeConfig {
actions?: ActionsConfig;
staticPaths?: StaticPath[];
}
interface EnvironmentConfig {
treatWarningsAsErrors: boolean;
minifyNames: boolean;
generateSourceMaps: boolean;
devServer?: DevServerConfig;
}
interface QuarcConfig {
environment: string;
serve?: ServeConfig;
environments: {
[key: string]: EnvironmentConfig;
};
}
function loadConfig(): QuarcConfig {
if (!fs.existsSync(configPath)) {
return {
environment: 'development',
environments: {
development: {
treatWarningsAsErrors: false,
minifyNames: false,
generateSourceMaps: true,
devServer: {
port: 4300,
},
},
},
};
}
const content = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(content) as QuarcConfig;
}
function getDevServerPort(): number {
const args = process.argv.slice(2);
const portIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
if (portIndex !== -1 && args[portIndex + 1]) {
const port = parseInt(args[portIndex + 1], 10);
if (!isNaN(port) && port > 0 && port < 65536) {
return port;
}
}
const config = loadConfig();
const envConfig = config.environments[config.environment];
return envConfig?.devServer?.port || 4300;
}
function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
};
return mimeTypes[ext] || 'application/octet-stream';
}
function getWebSocketConfig(): WebSocketConfig | undefined {
const config = loadConfig();
const envConfig = config.environments[config.environment];
return envConfig?.devServer?.websocket;
}
function attachWebSocketServer(server: http.Server): void {
wsServer = new WebSocketServer({ server, path: '/qu-ws/' });
wsServer.on('connection', (ws: WebSocket) => {
wsClients.add(ws);
console.log('Client connected to live reload WebSocket');
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
handleIncomingMessage(message, ws);
} catch {
// ignore invalid messages
}
});
ws.on('close', () => {
wsClients.delete(ws);
console.log('Client disconnected from live reload WebSocket');
});
ws.send(JSON.stringify({ type: 'connected' }));
});
console.log('WebSocket server attached to HTTP server');
connectToMergedSources();
}
function connectToMergedSources(): void {
const wsConfig = getWebSocketConfig();
const mergeFrom = wsConfig?.mergeFrom || [];
for (const url of mergeFrom) {
connectToMergedSource(url);
}
}
function connectToMergedSource(url: string): void {
console.log(`Connecting to merged WebSocket source: ${url}`);
const ws = new WebSocket(url);
ws.on('open', () => {
console.log(`Connected to merged source: ${url}`);
mergedWsConnections.push(ws);
});
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
if (message.type === 'reload') {
broadcastToClients(message);
}
} catch {
// ignore invalid messages
}
});
ws.on('close', () => {
console.log(`Disconnected from merged source: ${url}, reconnecting...`);
mergedWsConnections = mergedWsConnections.filter(c => c !== ws);
setTimeout(() => connectToMergedSource(url), 2000);
});
ws.on('error', (err: Error) => {
console.warn(`WebSocket error for ${url}:`, err.message);
});
}
function handleIncomingMessage(message: { type: string; [key: string]: unknown }, sender: WebSocket): void {
if (message.type === 'reload') {
broadcastToClients(message, sender);
broadcastToMergedSources(message);
}
}
function broadcastToClients(message: { type: string; [key: string]: unknown }, excludeSender?: WebSocket): void {
const data = JSON.stringify(message);
for (const client of wsClients) {
if (client !== excludeSender && client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
}
function broadcastToMergedSources(message: { type: string; [key: string]: unknown }): void {
const data = JSON.stringify(message);
for (const ws of mergedWsConnections) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
}
}
function getStaticPaths(): StaticPath[] {
const config = loadConfig();
return config.serve?.staticPaths || [];
}
function proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void {
const parsedUrl = new URL(targetUrl);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const proxyReq = protocol.request(
targetUrl,
{
method: req.method,
headers: {
...req.headers,
host: parsedUrl.host,
},
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
},
);
proxyReq.on('error', (err) => {
console.error('Proxy error:', err.message);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
}
function isRemotePath(staticPath: StaticPath): staticPath is StaticRemotePath {
return 'url' in staticPath;
}
function tryServeStaticPath(reqUrl: string, req: http.IncomingMessage, res: http.ServerResponse): boolean {
const staticPaths = getStaticPaths();
for (const staticPath of staticPaths) {
if (reqUrl.startsWith(staticPath.location)) {
const relativePath = reqUrl.slice(staticPath.location.length);
if (isRemotePath(staticPath)) {
const targetUrl = staticPath.url + relativePath;
proxyRequest(targetUrl, req, res);
return true;
}
const basePath = path.resolve(projectRoot, staticPath.path);
let filePath = path.join(basePath, relativePath || 'index.html');
const normalizedFilePath = path.normalize(filePath);
if (!normalizedFilePath.startsWith(basePath)) {
res.writeHead(403);
res.end('Forbidden');
return true;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, 'index.html');
}
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not Found');
return true;
}
const mimeType = getMimeType(filePath);
const content = fs.readFileSync(filePath);
res.writeHead(200, {
'Content-Type': mimeType,
'Cache-Control': 'no-cache',
});
res.end(content);
return true;
}
}
return false;
}
function startHttpServer(port: number): void {
httpServer = http.createServer((req, res) => {
const reqUrl = req.url || '/';
if (tryServeStaticPath(reqUrl, req, res)) {
return;
}
let filePath = path.join(distDir, reqUrl === '/' ? 'index.html' : reqUrl);
if (filePath.includes('..')) {
res.writeHead(403);
res.end('Forbidden');
return;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, 'index.html');
}
if (!fs.existsSync(filePath)) {
const indexPath = path.join(distDir, 'index.html');
if (fs.existsSync(indexPath)) {
filePath = indexPath;
} else {
res.writeHead(404);
res.end('Not Found');
return;
}
}
const mimeType = getMimeType(filePath);
const content = fs.readFileSync(filePath);
res.writeHead(200, {
'Content-Type': mimeType,
'Cache-Control': 'no-cache',
});
res.end(content);
});
httpServer.listen(port, () => {
console.log(`\n** Quarc Live Development Server is listening on localhost:${port} **`);
console.log(`** Open your browser on http://localhost:${port}/ **\n`);
});
attachWebSocketServer(httpServer);
}
function notifyClients(): void {
const message = { type: 'reload' };
broadcastToClients(message);
broadcastToMergedSources(message);
if (wsClients.size > 0) {
console.log('📢 Notified clients to reload');
}
}
async function runBuild(): Promise<void> {
if (isBuilding) {
buildQueued = true;
return;
}
isBuilding = true;
buildQueued = false;
console.log('\n🔨 Building application...');
const startTime = Date.now();
try {
const buildScript = path.join(__dirname, 'build.ts');
const tsNodePath = path.join(projectRoot, 'node_modules', '.bin', 'ts-node');
execSync(`${tsNodePath} ${buildScript}`, {
stdio: 'inherit',
cwd: projectRoot,
});
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`✅ Build completed in ${duration}s`);
notifyClients();
} catch (error) {
console.error('❌ Build failed');
} finally {
isBuilding = false;
if (buildQueued) {
console.log('⏳ Running queued build...');
setTimeout(() => runBuild(), 100);
}
}
}
function watchFiles(): void {
console.log(`👀 Watching for changes in ${srcDir}...`);
const debounceDelay = 300;
let debounceTimer: NodeJS.Timeout | null = null;
const watcher = fs.watch(srcDir, { recursive: true }, (eventType, filename) => {
if (!filename) return;
const ext = path.extname(filename);
if (!['.ts', '.scss', '.sass', '.css', '.html'].includes(ext)) {
return;
}
console.log(`📝 File changed: ${filename}`);
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
runBuild();
}, debounceDelay);
});
const cleanup = () => {
console.log('\n👋 Stopping watch mode...');
watcher.close();
for (const client of wsClients) {
client.close();
}
wsClients.clear();
for (const ws of mergedWsConnections) {
ws.close();
}
mergedWsConnections = [];
if (wsServer) {
wsServer.close();
}
if (httpServer) {
httpServer.close();
}
terminateActionProcesses();
runPostServeActions();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
function injectLiveReloadScript(): void {
const indexPath = path.join(distDir, 'index.html');
const wsPort = getDevServerPort();
if (!fs.existsSync(indexPath)) {
console.warn('index.html not found in dist directory');
return;
}
let html = fs.readFileSync(indexPath, 'utf-8');
const liveReloadScript = `
<script>
(function() {
let ws;
let reconnectAttempts = 0;
const maxReconnectDelay = 5000;
function connect() {
ws = new WebSocket('ws://localhost:${wsPort}/qu-ws/');
ws.onopen = () => {
console.log('[Live Reload] Connected');
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'reload') {
console.log('[Live Reload] Reloading page...');
window.location.reload();
}
} catch {}
};
ws.onclose = () => {
console.warn('[Live Reload] Connection lost, attempting to reconnect...');
reconnectAttempts++;
const delay = Math.min(1000 * reconnectAttempts, maxReconnectDelay);
setTimeout(connect, delay);
};
ws.onerror = () => {
ws.close();
};
}
connect();
})();
</script>
`;
if (!html.includes('Live Reload')) {
html = html.replace('</body>', `${liveReloadScript}</body>`);
fs.writeFileSync(indexPath, html, 'utf-8');
console.log('✅ Injected live reload script into index.html');
}
}
function runPreServeActions(): void {
const config = loadConfig();
const actions = config.serve?.actions?.preserve || [];
if (actions.length === 0) return;
console.log('🔧 Running preserve actions...');
for (const action of actions) {
console.log(`${action}`);
const child = spawn(action, [], {
shell: true,
cwd: projectRoot,
stdio: 'inherit',
detached: true,
});
actionProcesses.push(child);
child.on('error', (err) => {
console.error(` ❌ Action failed: ${action}`, err.message);
});
}
}
function runPostServeActions(): void {
const config = loadConfig();
const actions = config.serve?.actions?.postserve || [];
if (actions.length === 0) return;
console.log('🔧 Running postserve actions...');
for (const action of actions) {
console.log(`${action}`);
try {
execSync(action, {
cwd: projectRoot,
stdio: 'inherit',
});
} catch (err) {
console.error(` ❌ Action failed: ${action}`);
}
}
}
function terminateActionProcesses(): void {
if (actionProcesses.length === 0) return;
console.log('🛑 Terminating action processes...');
for (const child of actionProcesses) {
if (child.pid && !child.killed) {
try {
process.kill(-child.pid, 'SIGTERM');
console.log(` ✓ Terminated process group ${child.pid}`);
} catch (err) {
try {
child.kill('SIGTERM');
console.log(` ✓ Terminated process ${child.pid}`);
} catch {
console.warn(` ⚠ Could not terminate process ${child.pid}`);
}
}
}
}
actionProcesses = [];
}
async function serve(): Promise<void> {
const port = getDevServerPort();
console.log('🚀 Starting development server...\n');
runPreServeActions();
console.log('📦 Running initial build...');
await runBuild();
injectLiveReloadScript();
startHttpServer(port);
console.log('✨ Development server is ready!');
console.log('📂 Serving files from:', distDir);
console.log('🔄 Live reload WebSocket enabled on port', port);
console.log('\nPress Ctrl+C to stop\n');
watchFiles();
}
serve().catch(error => {
console.error('Serve process failed:', error);
process.exit(1);
});

76
cli/types.ts Normal file
View File

@ -0,0 +1,76 @@
export interface SizeThreshold {
warning: string;
error: string;
}
export interface EnvironmentConfig {
treatWarningsAsErrors: boolean;
minifyNames: boolean;
generateSourceMaps: boolean;
compressed?: boolean;
devServer?: DevServerConfig;
}
export interface DevServerConfig {
port: number;
websocket?: WebSocketConfig;
}
export interface WebSocketConfig {
mergeFrom?: string[];
}
export interface StaticLocalPath {
location: string;
path: string;
}
export interface StaticRemotePath {
location: string;
url: string;
}
export type StaticPath = StaticLocalPath | StaticRemotePath;
export interface ActionsConfig {
prebuild?: string[];
postbuild?: string[];
preserve?: string[];
postserve?: string[];
}
export interface BuildConfig {
actions?: ActionsConfig;
minifyNames: boolean;
scripts?: string[];
externalEntryPoints?: string[];
styles?: string[];
externalStyles?: string[];
limits: {
total: SizeThreshold;
main: SizeThreshold;
sourceMaps: SizeThreshold;
components?: SizeThreshold;
};
}
export interface ServeConfig {
actions?: ActionsConfig;
staticPaths?: StaticPath[];
}
export interface QuarcConfig {
environment: string;
build?: BuildConfig;
serve?: ServeConfig;
environments: {
[key: string]: EnvironmentConfig;
};
}
export interface ValidationResult {
status: 'success' | 'warning' | 'error';
message: string;
actual: number;
limit: number;
}

View File

@ -61,6 +61,18 @@ export class Injector {
}
public createInstanceWithProviders<T>(classType: Type<T>, providers: Provider[]): T {
console.log('[DI] createInstanceWithProviders START', {
classType,
typeofClassType: typeof classType,
isFunction: typeof classType === 'function',
hasName: classType?.name,
originalName: (classType as any)?.__quarc_original_name__,
providers: providers.map(p => ({
provide: typeof p.provide === 'string' ? p.provide : p.provide?.name,
type: 'useValue' in p ? 'useValue' : 'useClass' in p ? 'useClass' : 'useFactory' in p ? 'useFactory' : 'useExisting'
}))
});
if (!classType) {
throw new Error(`[DI] createInstanceWithProviders called with undefined classType`);
}
@ -175,21 +187,32 @@ export class Injector {
}
private resolveDependency(token: any, providers: Provider[]): any {
console.log('[DI] resolveDependency', {
token,
typeofToken: typeof token,
tokenName: typeof token === 'string' ? token : (token as any).__quarc_original_name__ || token?.name,
isFunction: typeof token === 'function',
});
const tokenName = typeof token === 'string' ? token : (token as any).__quarc_original_name__ || token.name;
const provider = this.findProvider(token, providers);
if (provider) {
console.log('[DI] Found provider for token', tokenName);
return this.resolveProviderValue(provider, providers);
}
if (this.sharedInstances[tokenName]) {
console.log('[DI] Found in sharedInstances', tokenName);
return this.sharedInstances[tokenName];
}
if (this.instanceCache[tokenName]) {
console.log('[DI] Found in instanceCache', tokenName);
return this.instanceCache[tokenName];
}
console.log('[DI] Creating new instance for token', tokenName);
return this.createInstanceWithProviders(token, providers);
}