bertui 0.4.0 → 0.4.2
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/package.json +4 -1
- package/src/build/image-optimizer.js +216 -0
- package/src/build.js +34 -60
- package/src/server/dev-server.js +90 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Lightning-fast React dev server powered by Bun and Elysia",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
"url": "https://github.com/BunElysiaReact/BERTUI.git"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@jsquash/jpeg": "^1.6.0",
|
|
48
|
+
"@jsquash/png": "^3.1.1",
|
|
49
|
+
"@jsquash/webp": "^1.5.0",
|
|
47
50
|
"elysia": "^1.0.0",
|
|
48
51
|
"ernest-logger": "latest",
|
|
49
52
|
"lightningcss": "^1.30.2"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// bertui/src/build/image-optimizer.js - WASM-POWERED VERSION 🚀
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, cpSync } from 'fs';
|
|
4
|
+
import logger from '../logger/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 🎯 WASM-powered image optimization using @jsquash
|
|
8
|
+
* Zero OS dependencies, pure JavaScript, blazing fast!
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Lazy-load WASM modules (only when needed)
|
|
12
|
+
let pngEncode, pngDecode;
|
|
13
|
+
let jpegEncode, jpegDecode;
|
|
14
|
+
let webpEncode, webpDecode;
|
|
15
|
+
|
|
16
|
+
async function initializePNG() {
|
|
17
|
+
if (!pngEncode) {
|
|
18
|
+
const { encode, decode } = await import('@jsquash/png');
|
|
19
|
+
pngEncode = encode;
|
|
20
|
+
pngDecode = decode;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function initializeJPEG() {
|
|
25
|
+
if (!jpegEncode) {
|
|
26
|
+
const { encode, decode } = await import('@jsquash/jpeg');
|
|
27
|
+
jpegEncode = encode;
|
|
28
|
+
jpegDecode = decode;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function initializeWebP() {
|
|
33
|
+
if (!webpEncode) {
|
|
34
|
+
const { encode, decode } = await import('@jsquash/webp');
|
|
35
|
+
webpEncode = encode;
|
|
36
|
+
webpDecode = decode;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optimize images using WASM-powered codecs
|
|
42
|
+
* This is FAST and has ZERO OS dependencies! 🚀
|
|
43
|
+
*/
|
|
44
|
+
export async function optimizeImages(srcDir, outDir) {
|
|
45
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg'];
|
|
46
|
+
let optimized = 0;
|
|
47
|
+
let totalSaved = 0;
|
|
48
|
+
|
|
49
|
+
logger.info('🖼️ Optimizing images with WASM codecs...');
|
|
50
|
+
|
|
51
|
+
async function processDirectory(dir, targetDir) {
|
|
52
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const srcPath = join(dir, entry.name);
|
|
56
|
+
const destPath = join(targetDir, entry.name);
|
|
57
|
+
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (!existsSync(destPath)) {
|
|
60
|
+
mkdirSync(destPath, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
await processDirectory(srcPath, destPath);
|
|
63
|
+
} else if (entry.isFile()) {
|
|
64
|
+
const ext = extname(entry.name).toLowerCase();
|
|
65
|
+
|
|
66
|
+
if (imageExtensions.includes(ext)) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await optimizeImage(srcPath, destPath);
|
|
69
|
+
if (result) {
|
|
70
|
+
optimized++;
|
|
71
|
+
totalSaved += result.saved;
|
|
72
|
+
const savedPercent = ((result.saved / result.originalSize) * 100).toFixed(1);
|
|
73
|
+
logger.debug(
|
|
74
|
+
`✨ ${entry.name}: ${(result.originalSize / 1024).toFixed(1)}KB → ${(result.newSize / 1024).toFixed(1)}KB (-${savedPercent}%)`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.warn(`⚠️ Failed to optimize ${entry.name}: ${error.message}`);
|
|
79
|
+
// Fallback: just copy the file
|
|
80
|
+
cpSync(srcPath, destPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await processDirectory(srcDir, outDir);
|
|
88
|
+
|
|
89
|
+
if (optimized > 0) {
|
|
90
|
+
logger.success(
|
|
91
|
+
`✅ Optimized ${optimized} images (saved ${(totalSaved / 1024).toFixed(2)}KB total)`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { optimized, saved: totalSaved };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Optimize a single image using WASM codecs
|
|
100
|
+
*/
|
|
101
|
+
async function optimizeImage(srcPath, destPath) {
|
|
102
|
+
const ext = extname(srcPath).toLowerCase();
|
|
103
|
+
const originalFile = Bun.file(srcPath);
|
|
104
|
+
const originalSize = originalFile.size;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// For SVG and GIF, just copy (no optimization needed/supported)
|
|
108
|
+
if (ext === '.svg' || ext === '.gif') {
|
|
109
|
+
cpSync(srcPath, destPath);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Read the original image
|
|
114
|
+
const originalBuffer = await originalFile.arrayBuffer();
|
|
115
|
+
|
|
116
|
+
let optimizedBuffer;
|
|
117
|
+
|
|
118
|
+
if (ext === '.png') {
|
|
119
|
+
await initializePNG();
|
|
120
|
+
|
|
121
|
+
// Decode → Re-encode with compression
|
|
122
|
+
const imageData = await pngDecode(originalBuffer);
|
|
123
|
+
|
|
124
|
+
// Encode with oxipng-level compression (quality 85, similar to oxipng -o 2)
|
|
125
|
+
optimizedBuffer = await pngEncode(imageData, {
|
|
126
|
+
quality: 85,
|
|
127
|
+
effort: 2 // 0-10, higher = better compression but slower
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
} else if (ext === '.jpg' || ext === '.jpeg') {
|
|
131
|
+
await initializeJPEG();
|
|
132
|
+
|
|
133
|
+
// Decode → Re-encode with quality 85 (mozjpeg-like quality)
|
|
134
|
+
const imageData = await jpegDecode(originalBuffer);
|
|
135
|
+
optimizedBuffer = await jpegEncode(imageData, { quality: 85 });
|
|
136
|
+
|
|
137
|
+
} else if (ext === '.webp') {
|
|
138
|
+
await initializeWebP();
|
|
139
|
+
|
|
140
|
+
// WebP optimization
|
|
141
|
+
const imageData = await webpDecode(originalBuffer);
|
|
142
|
+
optimizedBuffer = await webpEncode(imageData, { quality: 85 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Only save if we actually reduced the size
|
|
146
|
+
if (optimizedBuffer && optimizedBuffer.byteLength < originalSize) {
|
|
147
|
+
await Bun.write(destPath, optimizedBuffer);
|
|
148
|
+
const saved = originalSize - optimizedBuffer.byteLength;
|
|
149
|
+
return { saved, originalSize, newSize: optimizedBuffer.byteLength };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If optimization didn't help, just copy the original
|
|
153
|
+
cpSync(srcPath, destPath);
|
|
154
|
+
return null;
|
|
155
|
+
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// If anything fails, just copy the original
|
|
158
|
+
logger.warn(`Optimization failed for ${srcPath.split('/').pop()}, copying original`);
|
|
159
|
+
cpSync(srcPath, destPath);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if optimization is available (always true with WASM! 🎉)
|
|
166
|
+
*/
|
|
167
|
+
export async function checkOptimizationTools() {
|
|
168
|
+
try {
|
|
169
|
+
// Try to import the WASM modules
|
|
170
|
+
await import('@jsquash/png');
|
|
171
|
+
await import('@jsquash/jpeg');
|
|
172
|
+
await import('@jsquash/webp');
|
|
173
|
+
|
|
174
|
+
logger.success('✅ WASM image optimization available');
|
|
175
|
+
logger.info('📦 Using @jsquash (zero OS dependencies!)');
|
|
176
|
+
return ['png', 'jpeg', 'webp'];
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error('❌ WASM codecs not installed. Run: bun add @jsquash/png @jsquash/jpeg @jsquash/webp');
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Copy images without optimization (fallback)
|
|
185
|
+
*/
|
|
186
|
+
export function copyImages(srcDir, outDir) {
|
|
187
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif'];
|
|
188
|
+
let copied = 0;
|
|
189
|
+
|
|
190
|
+
function processDirectory(dir, targetDir) {
|
|
191
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
192
|
+
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const srcPath = join(dir, entry.name);
|
|
195
|
+
const destPath = join(targetDir, entry.name);
|
|
196
|
+
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
if (!existsSync(destPath)) {
|
|
199
|
+
mkdirSync(destPath, { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
processDirectory(srcPath, targetDir);
|
|
202
|
+
} else if (entry.isFile()) {
|
|
203
|
+
const ext = extname(entry.name).toLowerCase();
|
|
204
|
+
|
|
205
|
+
if (imageExtensions.includes(ext)) {
|
|
206
|
+
cpSync(srcPath, destPath);
|
|
207
|
+
copied++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
processDirectory(srcDir, outDir);
|
|
214
|
+
logger.info(`📋 Copied ${copied} images without optimization`);
|
|
215
|
+
return copied;
|
|
216
|
+
}
|
package/src/build.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs
|
|
|
3
3
|
import logger from './logger/logger.js';
|
|
4
4
|
import { buildCSS } from './build/css-builder.js';
|
|
5
5
|
import { loadEnvVariables, replaceEnvInCode } from './utils/env.js';
|
|
6
|
+
import { optimizeImages, checkOptimizationTools, copyImages } from './build/image-optimizer.js';
|
|
6
7
|
|
|
7
8
|
export async function buildProduction(options = {}) {
|
|
8
9
|
const root = options.root || process.cwd();
|
|
@@ -38,11 +39,20 @@ export async function buildProduction(options = {}) {
|
|
|
38
39
|
logger.info('Step 2: Building CSS with Lightning CSS...');
|
|
39
40
|
await buildAllCSS(root, outDir);
|
|
40
41
|
|
|
41
|
-
// ✅
|
|
42
|
-
logger.info('Step 3:
|
|
43
|
-
await
|
|
42
|
+
// ✅ NEW: Check if image optimization is available
|
|
43
|
+
logger.info('Step 3: Checking image optimization tools...');
|
|
44
|
+
const optimizationTools = await checkOptimizationTools();
|
|
44
45
|
|
|
45
|
-
logger.info('Step 4:
|
|
46
|
+
logger.info('Step 4: Copying and optimizing static assets...');
|
|
47
|
+
if (optimizationTools.length > 0) {
|
|
48
|
+
// Use WASM-powered optimization
|
|
49
|
+
await copyAllStaticAssets(root, outDir, true);
|
|
50
|
+
} else {
|
|
51
|
+
// Fallback: just copy images
|
|
52
|
+
await copyAllStaticAssets(root, outDir, false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.info('Step 5: Bundling JavaScript with Bun...');
|
|
46
56
|
const buildEntry = join(buildDir, 'main.js');
|
|
47
57
|
|
|
48
58
|
if (!existsSync(buildEntry)) {
|
|
@@ -85,7 +95,7 @@ export async function buildProduction(options = {}) {
|
|
|
85
95
|
|
|
86
96
|
logger.success('JavaScript bundled with tree-shaking');
|
|
87
97
|
|
|
88
|
-
logger.info('Step
|
|
98
|
+
logger.info('Step 6: Generating SEO-optimized HTML files...');
|
|
89
99
|
await generateProductionHTML(root, outDir, result, routes);
|
|
90
100
|
|
|
91
101
|
rmSync(buildDir, { recursive: true });
|
|
@@ -121,76 +131,42 @@ export async function buildProduction(options = {}) {
|
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
// ✅
|
|
125
|
-
async function copyAllStaticAssets(root, outDir) {
|
|
134
|
+
// ✅ UPDATED: Copy and optionally optimize static assets
|
|
135
|
+
async function copyAllStaticAssets(root, outDir, optimize = true) {
|
|
126
136
|
const publicDir = join(root, 'public');
|
|
127
137
|
const srcDir = join(root, 'src');
|
|
128
138
|
|
|
129
139
|
let assetsCopied = 0;
|
|
140
|
+
let assetsOptimized = 0;
|
|
130
141
|
|
|
131
142
|
// Copy from public/
|
|
132
143
|
if (existsSync(publicDir)) {
|
|
133
|
-
|
|
144
|
+
if (optimize) {
|
|
145
|
+
const result = await optimizeImages(publicDir, outDir);
|
|
146
|
+
assetsOptimized += result.optimized;
|
|
147
|
+
} else {
|
|
148
|
+
assetsCopied += copyImages(publicDir, outDir);
|
|
149
|
+
}
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
// Copy static assets from src/ (images, fonts, etc.)
|
|
137
153
|
if (existsSync(srcDir)) {
|
|
138
154
|
const assetsOutDir = join(outDir, 'assets');
|
|
139
155
|
mkdirSync(assetsOutDir, { recursive: true });
|
|
140
|
-
assetsCopied += await copyStaticAssetsFromDir(srcDir, assetsOutDir, 'src', true);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
logger.success(`Copied ${assetsCopied} static assets`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ✅ NEW FUNCTION: Recursively copy static assets
|
|
147
|
-
async function copyStaticAssetsFromDir(sourceDir, targetDir, label, skipStyles = false) {
|
|
148
|
-
const staticExtensions = [
|
|
149
|
-
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', // Images
|
|
150
|
-
'.woff', '.woff2', '.ttf', '.otf', '.eot', // Fonts
|
|
151
|
-
'.mp4', '.webm', '.ogg', '.mp3', '.wav', // Media
|
|
152
|
-
'.pdf', '.zip', '.json', '.xml', '.txt' // Documents
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
let copiedCount = 0;
|
|
156
|
-
|
|
157
|
-
function copyRecursive(dir, targetBase) {
|
|
158
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (entry.isDirectory()) {
|
|
166
|
-
// Skip node_modules, .bertui, etc.
|
|
167
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Skip styles directory if requested
|
|
172
|
-
if (skipStyles && entry.name === 'styles') {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
mkdirSync(destPath, { recursive: true });
|
|
177
|
-
copyRecursive(srcPath, targetBase);
|
|
178
|
-
} else if (entry.isFile()) {
|
|
179
|
-
const ext = extname(entry.name);
|
|
180
|
-
|
|
181
|
-
// Copy static assets only
|
|
182
|
-
if (staticExtensions.includes(ext.toLowerCase())) {
|
|
183
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
184
|
-
cpSync(srcPath, destPath);
|
|
185
|
-
logger.debug(`Copied ${label}/${relativePath}`);
|
|
186
|
-
copiedCount++;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
157
|
+
if (optimize) {
|
|
158
|
+
const result = await optimizeImages(srcDir, assetsOutDir);
|
|
159
|
+
assetsOptimized += result.optimized;
|
|
160
|
+
} else {
|
|
161
|
+
assetsCopied += copyImages(srcDir, assetsOutDir);
|
|
189
162
|
}
|
|
190
163
|
}
|
|
191
164
|
|
|
192
|
-
|
|
193
|
-
|
|
165
|
+
if (optimize && assetsOptimized > 0) {
|
|
166
|
+
logger.success(`🎨 Optimized ${assetsOptimized} images with WASM codecs`);
|
|
167
|
+
} else {
|
|
168
|
+
logger.success(`📋 Copied ${assetsCopied} static assets`);
|
|
169
|
+
}
|
|
194
170
|
}
|
|
195
171
|
|
|
196
172
|
async function buildAllCSS(root, outDir) {
|
|
@@ -447,7 +423,6 @@ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
|
|
|
447
423
|
code = replaceEnvInCode(code, envVars);
|
|
448
424
|
code = fixBuildImports(code, srcPath, outPath, root);
|
|
449
425
|
|
|
450
|
-
// ✅ FIX: Add React import if needed
|
|
451
426
|
if (usesJSX(code) && !code.includes('import React')) {
|
|
452
427
|
code = `import React from 'react';\n${code}`;
|
|
453
428
|
}
|
|
@@ -486,7 +461,6 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
|
|
|
486
461
|
|
|
487
462
|
let compiled = await transpiler.transform(code);
|
|
488
463
|
|
|
489
|
-
// ✅ FIX: Add React import if needed
|
|
490
464
|
if (usesJSX(compiled) && !compiled.includes('import React')) {
|
|
491
465
|
compiled = `import React from 'react';\n${compiled}`;
|
|
492
466
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// src/server/dev-server.js - FIXED VERSION
|
|
1
2
|
import { Elysia } from 'elysia';
|
|
2
3
|
import { watch } from 'fs';
|
|
3
4
|
import { join, extname } from 'path';
|
|
@@ -11,6 +12,7 @@ export async function startDevServer(options = {}) {
|
|
|
11
12
|
const root = options.root || process.cwd();
|
|
12
13
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
13
14
|
const stylesDir = join(root, '.bertui', 'styles');
|
|
15
|
+
const srcDir = join(root, 'src');
|
|
14
16
|
|
|
15
17
|
const config = await loadConfig(root);
|
|
16
18
|
|
|
@@ -28,6 +30,49 @@ export async function startDevServer(options = {}) {
|
|
|
28
30
|
return serveHTML(root, hasRouter, config);
|
|
29
31
|
})
|
|
30
32
|
|
|
33
|
+
// ✅ NEW: Serve images from src/images/
|
|
34
|
+
.get('/images/*', async ({ params, set }) => {
|
|
35
|
+
const imagesDir = join(srcDir, 'images');
|
|
36
|
+
const filepath = join(imagesDir, params['*']);
|
|
37
|
+
const file = Bun.file(filepath);
|
|
38
|
+
|
|
39
|
+
if (!await file.exists()) {
|
|
40
|
+
set.status = 404;
|
|
41
|
+
return 'Image not found';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ext = extname(filepath).toLowerCase();
|
|
45
|
+
const contentType = getImageContentType(ext);
|
|
46
|
+
|
|
47
|
+
return new Response(file, {
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': contentType,
|
|
50
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ✅ NEW: Serve any static asset from src/ (fonts, videos, etc.)
|
|
56
|
+
.get('/assets/*', async ({ params, set }) => {
|
|
57
|
+
const filepath = join(srcDir, params['*']);
|
|
58
|
+
const file = Bun.file(filepath);
|
|
59
|
+
|
|
60
|
+
if (!await file.exists()) {
|
|
61
|
+
set.status = 404;
|
|
62
|
+
return 'Asset not found';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ext = extname(filepath).toLowerCase();
|
|
66
|
+
const contentType = getContentType(ext);
|
|
67
|
+
|
|
68
|
+
return new Response(file, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': contentType,
|
|
71
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
})
|
|
75
|
+
|
|
31
76
|
.get('/*', async ({ params, set }) => {
|
|
32
77
|
const path = params['*'];
|
|
33
78
|
|
|
@@ -413,6 +458,7 @@ ws.onclose = () => {
|
|
|
413
458
|
|
|
414
459
|
logger.success(`🚀 Server running at http://localhost:${port}`);
|
|
415
460
|
logger.info(`📁 Serving: ${root}`);
|
|
461
|
+
logger.info(`🖼️ Images available at: /images/*`);
|
|
416
462
|
|
|
417
463
|
setupWatcher(root, compiledDir, clients, async () => {
|
|
418
464
|
hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
@@ -490,6 +536,22 @@ ${userStylesheets}
|
|
|
490
536
|
});
|
|
491
537
|
}
|
|
492
538
|
|
|
539
|
+
// ✅ NEW: Helper for image content types
|
|
540
|
+
function getImageContentType(ext) {
|
|
541
|
+
const types = {
|
|
542
|
+
'.jpg': 'image/jpeg',
|
|
543
|
+
'.jpeg': 'image/jpeg',
|
|
544
|
+
'.png': 'image/png',
|
|
545
|
+
'.gif': 'image/gif',
|
|
546
|
+
'.svg': 'image/svg+xml',
|
|
547
|
+
'.webp': 'image/webp',
|
|
548
|
+
'.avif': 'image/avif',
|
|
549
|
+
'.ico': 'image/x-icon'
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
return types[ext] || 'application/octet-stream';
|
|
553
|
+
}
|
|
554
|
+
|
|
493
555
|
function getContentType(ext) {
|
|
494
556
|
const types = {
|
|
495
557
|
'.js': 'application/javascript',
|
|
@@ -502,7 +564,16 @@ function getContentType(ext) {
|
|
|
502
564
|
'.jpeg': 'image/jpeg',
|
|
503
565
|
'.gif': 'image/gif',
|
|
504
566
|
'.svg': 'image/svg+xml',
|
|
505
|
-
'.
|
|
567
|
+
'.webp': 'image/webp',
|
|
568
|
+
'.avif': 'image/avif',
|
|
569
|
+
'.ico': 'image/x-icon',
|
|
570
|
+
'.woff': 'font/woff',
|
|
571
|
+
'.woff2': 'font/woff2',
|
|
572
|
+
'.ttf': 'font/ttf',
|
|
573
|
+
'.otf': 'font/otf',
|
|
574
|
+
'.mp4': 'video/mp4',
|
|
575
|
+
'.webm': 'video/webm',
|
|
576
|
+
'.mp3': 'audio/mpeg'
|
|
506
577
|
};
|
|
507
578
|
|
|
508
579
|
return types[ext] || 'text/plain';
|
|
@@ -523,9 +594,26 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
|
523
594
|
if (!filename) return;
|
|
524
595
|
|
|
525
596
|
const ext = extname(filename);
|
|
526
|
-
|
|
597
|
+
|
|
598
|
+
// ✅ Watch image changes too
|
|
599
|
+
const watchedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'];
|
|
600
|
+
|
|
601
|
+
if (watchedExtensions.includes(ext)) {
|
|
527
602
|
logger.info(`📝 File changed: ${filename}`);
|
|
528
603
|
|
|
604
|
+
// For images, just reload without recompiling
|
|
605
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
|
|
606
|
+
for (const client of clients) {
|
|
607
|
+
try {
|
|
608
|
+
client.send(JSON.stringify({ type: 'reload', file: filename }));
|
|
609
|
+
} catch (e) {
|
|
610
|
+
clients.delete(client);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// For code/CSS, recompile
|
|
529
617
|
for (const client of clients) {
|
|
530
618
|
try {
|
|
531
619
|
client.send(JSON.stringify({ type: 'recompiling' }));
|
|
@@ -551,7 +639,6 @@ function setupWatcher(root, compiledDir, clients, onRecompile) {
|
|
|
551
639
|
} catch (error) {
|
|
552
640
|
logger.error(`Recompilation failed: ${error.message}`);
|
|
553
641
|
|
|
554
|
-
// Send compilation error to clients
|
|
555
642
|
for (const client of clients) {
|
|
556
643
|
try {
|
|
557
644
|
client.send(JSON.stringify({
|