vatts 1.1.1 → 1.1.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/dist/builder.js +55 -7
- package/dist/client/client.d.ts +2 -0
- package/dist/client/client.js +8 -1
- package/dist/client/image/Image.d.ts +10 -0
- package/dist/client/image/Image.js +35 -0
- package/dist/index.js +142 -1
- package/package.json +4 -2
package/dist/builder.js
CHANGED
|
@@ -20,6 +20,14 @@ const path = require('path');
|
|
|
20
20
|
const Console = require("./api/console").default;
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const { readdir, stat, rm } = require("node:fs/promises");
|
|
23
|
+
// Tenta carregar o Sharp para otimização de imagens
|
|
24
|
+
let sharp;
|
|
25
|
+
try {
|
|
26
|
+
sharp = require('sharp');
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
Console.warn("Sharp não encontrado. Otimização de imagem (resize) desativada. Instale com: npm install sharp");
|
|
30
|
+
}
|
|
23
31
|
// Plugins Oficiais do Rollup
|
|
24
32
|
const nodeResolve = require('@rollup/plugin-node-resolve').default;
|
|
25
33
|
const commonjs = require('@rollup/plugin-commonjs').default;
|
|
@@ -202,8 +210,8 @@ const customPostCssPlugin = (isProduction) => {
|
|
|
202
210
|
};
|
|
203
211
|
};
|
|
204
212
|
/**
|
|
205
|
-
* Plugin Inteligente para Assets (Otimizado para RAM)
|
|
206
|
-
* -
|
|
213
|
+
* Plugin Inteligente para Assets (Otimizado para RAM e agora com Sharp)
|
|
214
|
+
* - Suporta redimensionamento via query params: import img from './image.png?w=200&h=200'
|
|
207
215
|
*/
|
|
208
216
|
const smartAssetPlugin = (isProduction) => {
|
|
209
217
|
// 4KB - Arquivos maiores que isso viram referência externa.
|
|
@@ -212,7 +220,8 @@ const smartAssetPlugin = (isProduction) => {
|
|
|
212
220
|
return {
|
|
213
221
|
name: 'smart-asset-loader',
|
|
214
222
|
async load(id) {
|
|
215
|
-
|
|
223
|
+
// Separa o ID dos parâmetros de query
|
|
224
|
+
const [cleanId, queryParams] = id.split('?');
|
|
216
225
|
if (cleanId.startsWith('\0'))
|
|
217
226
|
return null;
|
|
218
227
|
const ext = path.extname(cleanId).slice(1).toLowerCase();
|
|
@@ -236,6 +245,46 @@ const smartAssetPlugin = (isProduction) => {
|
|
|
236
245
|
return `export default ${JSON.stringify(content)};`;
|
|
237
246
|
}
|
|
238
247
|
let buffer = await fs.promises.readFile(cleanId);
|
|
248
|
+
// --- VATTS IMAGE OPTIMIZATION (SHARP) ---
|
|
249
|
+
// Verifica se tem sharp e se tem parâmetros de resize (w=, h=, q=)
|
|
250
|
+
if (sharp && queryParams && ['png', 'jpg', 'jpeg', 'webp', 'avif'].includes(ext)) {
|
|
251
|
+
try {
|
|
252
|
+
const params = new URLSearchParams(queryParams);
|
|
253
|
+
const width = params.get('w') || params.get('width');
|
|
254
|
+
const height = params.get('h') || params.get('height');
|
|
255
|
+
const quality = params.get('q') || params.get('quality');
|
|
256
|
+
if (width || height || quality) {
|
|
257
|
+
let transformer = sharp(buffer);
|
|
258
|
+
if (width || height) {
|
|
259
|
+
transformer = transformer.resize({
|
|
260
|
+
width: width ? parseInt(width) : null,
|
|
261
|
+
height: height ? parseInt(height) : null,
|
|
262
|
+
fit: 'cover', // Padrão 'cover' para manter aspect ratio preenchendo
|
|
263
|
+
withoutEnlargement: true // Não aumenta se a imagem original for menor
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Aplica qualidade/compressão se solicitado
|
|
267
|
+
if (quality) {
|
|
268
|
+
const q = parseInt(quality);
|
|
269
|
+
if (ext === 'jpeg' || ext === 'jpg')
|
|
270
|
+
transformer.jpeg({ quality: q });
|
|
271
|
+
if (ext === 'webp')
|
|
272
|
+
transformer.webp({ quality: q });
|
|
273
|
+
if (ext === 'avif')
|
|
274
|
+
transformer.avif({ quality: q });
|
|
275
|
+
if (ext === 'png')
|
|
276
|
+
transformer.png({ quality: q });
|
|
277
|
+
}
|
|
278
|
+
buffer = await transformer.toBuffer();
|
|
279
|
+
// Atualiza o tamanho para a decisão de inline/emit abaixo
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
Console.warn(`Failed to optimize image ${path.basename(cleanId)}:`, e.message);
|
|
284
|
+
// Falha silenciosa: usa o buffer original
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ------------------------------------------
|
|
239
288
|
const size = buffer.length;
|
|
240
289
|
// Tratamento especial para SVG (inline SVG vs URL)
|
|
241
290
|
if (type === 'svg') {
|
|
@@ -263,18 +312,17 @@ const smartAssetPlugin = (isProduction) => {
|
|
|
263
312
|
}
|
|
264
313
|
}
|
|
265
314
|
// Para outros assets:
|
|
266
|
-
// Se for pequeno
|
|
267
|
-
// Se for grande, Arquivo (reduz uso de RAM e tamanho do bundle JS)
|
|
268
|
-
// Essa lógica agora aplica para DEV e PROD. Base64 em Dev para arquivos grandes era o vilão da RAM.
|
|
315
|
+
// Se for pequeno (ou ficou pequeno após resize), Base64
|
|
269
316
|
if (size < INLINE_LIMIT) {
|
|
270
317
|
const base64 = buffer.toString('base64');
|
|
271
318
|
buffer = null; // Libera memória do buffer bruto imediatamente
|
|
272
319
|
return `export default "data:${type};base64,${base64}";`;
|
|
273
320
|
}
|
|
274
321
|
else {
|
|
322
|
+
// Se ainda for grande, emite arquivo
|
|
275
323
|
const referenceId = this.emitFile({
|
|
276
324
|
type: 'asset',
|
|
277
|
-
name: path.basename(cleanId),
|
|
325
|
+
name: path.basename(cleanId), // O nome será hasheado pelo Rollup output options
|
|
278
326
|
source: buffer
|
|
279
327
|
});
|
|
280
328
|
buffer = null; // Libera memória
|
package/dist/client/client.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { Link } from '../components/Link';
|
|
|
2
2
|
export { RouteConfig, Metadata } from "../types";
|
|
3
3
|
export { router } from './clientRouter';
|
|
4
4
|
export { importServer } from './rpc';
|
|
5
|
+
export { default as Image } from "./image/Image";
|
|
6
|
+
export { default as VattsImage } from "./image/Image";
|
package/dist/client/client.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
18
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.importServer = exports.router = exports.Link = void 0;
|
|
22
|
+
exports.VattsImage = exports.Image = exports.importServer = exports.router = exports.Link = void 0;
|
|
20
23
|
// Este arquivo exporta apenas código seguro para o cliente (navegador)
|
|
21
24
|
var Link_1 = require("../components/Link");
|
|
22
25
|
Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return Link_1.Link; } });
|
|
@@ -25,3 +28,7 @@ Object.defineProperty(exports, "router", { enumerable: true, get: function () {
|
|
|
25
28
|
// RPC (client-side)
|
|
26
29
|
var rpc_1 = require("./rpc");
|
|
27
30
|
Object.defineProperty(exports, "importServer", { enumerable: true, get: function () { return rpc_1.importServer; } });
|
|
31
|
+
var Image_1 = require("./image/Image");
|
|
32
|
+
Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return __importDefault(Image_1).default; } });
|
|
33
|
+
var Image_2 = require("./image/Image");
|
|
34
|
+
Object.defineProperty(exports, "VattsImage", { enumerable: true, get: function () { return __importDefault(Image_2).default; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface VattsImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
3
|
+
src: string;
|
|
4
|
+
width?: number | string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
quality?: number;
|
|
7
|
+
priority?: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const Image: React.FC<VattsImageProps>;
|
|
10
|
+
export default Image;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const Image = ({ src, width, height, quality = 75, priority = false, className, style, alt = "", ...props }) => {
|
|
5
|
+
// Se a imagem for Base64 (pequena) ou externa (http), não otimizamos via backend local
|
|
6
|
+
const isOptimizable = src && !src.startsWith('data:') && !src.startsWith('http');
|
|
7
|
+
let optimizedSrc = src;
|
|
8
|
+
if (isOptimizable) {
|
|
9
|
+
const params = new URLSearchParams();
|
|
10
|
+
params.set('url', src);
|
|
11
|
+
// Tratamento inteligente para remover "px" se o usuário passar string
|
|
12
|
+
if (width) {
|
|
13
|
+
const w = String(width).replace('px', '');
|
|
14
|
+
if (!isNaN(Number(w)))
|
|
15
|
+
params.set('w', w);
|
|
16
|
+
}
|
|
17
|
+
if (height) {
|
|
18
|
+
const h = String(height).replace('px', '');
|
|
19
|
+
if (!isNaN(Number(h)))
|
|
20
|
+
params.set('h', h);
|
|
21
|
+
}
|
|
22
|
+
if (quality)
|
|
23
|
+
params.set('q', quality.toString());
|
|
24
|
+
optimizedSrc = `/_vatts/image?${params.toString()}`;
|
|
25
|
+
}
|
|
26
|
+
// Estilos base para prevenir layout shift se dimensões forem fornecidas
|
|
27
|
+
const baseStyle = {
|
|
28
|
+
// Se width for numérico, assume pixels, senão usa o valor string (ex: 100%)
|
|
29
|
+
width: width ? (typeof width === 'number' ? `${width}px` : width) : 'auto',
|
|
30
|
+
height: height ? (typeof height === 'number' ? `${height}px` : height) : 'auto',
|
|
31
|
+
...style,
|
|
32
|
+
};
|
|
33
|
+
return ((0, jsx_runtime_1.jsx)("img", { ...props, src: optimizedSrc, alt: alt, loading: priority ? 'eager' : 'lazy', decoding: priority ? 'sync' : 'async', width: typeof width === 'string' ? width.replace('px', '') : width, height: typeof height === 'string' ? height.replace('px', '') : height, className: `vatts-image ${className || ''}`, style: baseStyle }));
|
|
34
|
+
};
|
|
35
|
+
exports.default = Image;
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ exports.app = exports.FrameworkAdapterFactory = exports.FastifyAdapter = exports
|
|
|
56
56
|
exports.default = vatts;
|
|
57
57
|
const path_1 = __importDefault(require("path"));
|
|
58
58
|
const fs_1 = __importDefault(require("fs"));
|
|
59
|
+
const crypto_1 = __importDefault(require("crypto")); // Adicionado para gerar hash do cache
|
|
59
60
|
const express_1 = require("./adapters/express");
|
|
60
61
|
const builder_1 = require("./builder");
|
|
61
62
|
const router_1 = require("./router");
|
|
@@ -90,6 +91,139 @@ function resolveWithin(baseDir, unsafePath) {
|
|
|
90
91
|
return null;
|
|
91
92
|
return abs;
|
|
92
93
|
}
|
|
94
|
+
// Handler de Otimização de Imagem
|
|
95
|
+
async function handleImageOptimization(req, res, projectDir) {
|
|
96
|
+
const urlObj = new URL(req.url, `http://localhost`);
|
|
97
|
+
const params = urlObj.searchParams;
|
|
98
|
+
const imageUrl = params.get('url');
|
|
99
|
+
const widthStr = params.get('w');
|
|
100
|
+
const heightStr = params.get('h');
|
|
101
|
+
const qualityStr = params.get('q');
|
|
102
|
+
if (!imageUrl) {
|
|
103
|
+
res.status(400).text('Missing "url" parameter');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isSuspiciousPathname(imageUrl)) {
|
|
107
|
+
res.status(400).text('Invalid path');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Resolve o caminho do arquivo no disco
|
|
111
|
+
let filePath = null;
|
|
112
|
+
if (imageUrl.startsWith('/_vatts/')) {
|
|
113
|
+
// Arquivos de build (assets gerados pelo Rollup)
|
|
114
|
+
const relPath = imageUrl.replace('/_vatts/', '');
|
|
115
|
+
filePath = resolveWithin(path_1.default.join(projectDir, '.vatts'), relPath);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Arquivos públicos
|
|
119
|
+
filePath = resolveWithin(path_1.default.join(projectDir, 'public'), imageUrl);
|
|
120
|
+
}
|
|
121
|
+
if (!filePath || !fs_1.default.existsSync(filePath)) {
|
|
122
|
+
res.status(404).text('Image not found');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Cache headers agressivos (o navegador deve cachear isso por muito tempo)
|
|
126
|
+
res.header('Cache-Control', 'public, max-age=31536000, immutable');
|
|
127
|
+
res.header('Vary', 'Accept');
|
|
128
|
+
// Tenta carregar o sharp
|
|
129
|
+
let sharp;
|
|
130
|
+
try {
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
sharp = require('sharp');
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
// Se não tiver sharp, avisa uma vez e serve o arquivo original
|
|
136
|
+
if (!global.__vatts_sharp_warned) {
|
|
137
|
+
console_1.default.warn('Package "sharp" not found. Image optimization is disabled. Install it with: npm install sharp');
|
|
138
|
+
global.__vatts_sharp_warned = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Se não tiver Sharp ou parâmetros, serve original
|
|
142
|
+
if (!sharp || (!widthStr && !heightStr && !qualityStr)) {
|
|
143
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
144
|
+
const contentTypes = {
|
|
145
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
146
|
+
'.webp': 'image/webp', '.avif': 'image/avif', '.gif': 'image/gif',
|
|
147
|
+
'.svg': 'image/svg+xml'
|
|
148
|
+
};
|
|
149
|
+
res.header('Content-Type', contentTypes[ext] || 'application/octet-stream');
|
|
150
|
+
const fileBuffer = await fs_1.default.promises.readFile(filePath);
|
|
151
|
+
res.send(fileBuffer);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
// --- SISTEMA DE CACHE EM DISCO (TEMPORÁRIO EM .vatts) ---
|
|
156
|
+
const cacheDir = path_1.default.join(projectDir, '.vatts', 'cache', 'images');
|
|
157
|
+
// Garante que a pasta existe (síncrono na primeira vez é ok, ou async)
|
|
158
|
+
if (!fs_1.default.existsSync(cacheDir)) {
|
|
159
|
+
fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
const width = widthStr ? parseInt(widthStr, 10) : undefined;
|
|
162
|
+
const height = heightStr ? parseInt(heightStr, 10) : undefined;
|
|
163
|
+
const quality = qualityStr ? parseInt(qualityStr, 10) : 75;
|
|
164
|
+
// Gera um hash único baseado em TODOS os parâmetros
|
|
165
|
+
// Ex: /img.png?w=100&h=200&q=80 -> hash unico
|
|
166
|
+
const cacheKey = `${imageUrl}?w=${width}&h=${height}&q=${quality}`;
|
|
167
|
+
const hash = crypto_1.default.createHash('md5').update(cacheKey).digest('hex');
|
|
168
|
+
// Define a extensão do arquivo de cache
|
|
169
|
+
const extOriginal = path_1.default.extname(filePath).toLowerCase();
|
|
170
|
+
let extOutput = '.webp'; // Padrão é converter para WebP
|
|
171
|
+
// Exceções que não viram WebP
|
|
172
|
+
if (extOriginal === '.svg' || extOriginal === '.gif') {
|
|
173
|
+
extOutput = extOriginal;
|
|
174
|
+
}
|
|
175
|
+
const cachedFilePath = path_1.default.join(cacheDir, `${hash}${extOutput}`);
|
|
176
|
+
// 1. VERIFICA SE JÁ EXISTE NO CACHE
|
|
177
|
+
if (fs_1.default.existsSync(cachedFilePath)) {
|
|
178
|
+
// Serve direto do cache (Disco)
|
|
179
|
+
// console.log(`[Vatts] Serving cached image: ${imageUrl}`);
|
|
180
|
+
const contentTypes = {
|
|
181
|
+
'.webp': 'image/webp',
|
|
182
|
+
'.svg': 'image/svg+xml',
|
|
183
|
+
'.gif': 'image/gif'
|
|
184
|
+
};
|
|
185
|
+
res.header('Content-Type', contentTypes[extOutput] || 'application/octet-stream');
|
|
186
|
+
const cachedBuffer = await fs_1.default.promises.readFile(cachedFilePath);
|
|
187
|
+
res.send(cachedBuffer);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// 2. SE NÃO EXISTIR, PROCESSA COM SHARP
|
|
191
|
+
// console.log(`[Vatts] Processing image: ${imageUrl}`);
|
|
192
|
+
const transformer = sharp(filePath);
|
|
193
|
+
transformer.rotate();
|
|
194
|
+
const isValidWidth = width && !isNaN(width);
|
|
195
|
+
const isValidHeight = height && !isNaN(height);
|
|
196
|
+
if (isValidWidth || isValidHeight) {
|
|
197
|
+
transformer.resize(isValidWidth ? width : null, isValidHeight ? height : null, { withoutEnlargement: true });
|
|
198
|
+
}
|
|
199
|
+
if (extOriginal === '.png' || extOriginal === '.jpg' || extOriginal === '.jpeg' || extOriginal === '.webp') {
|
|
200
|
+
transformer.webp({ quality });
|
|
201
|
+
res.header('Content-Type', 'image/webp');
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Outros tipos mantêm original
|
|
205
|
+
const contentTypes = {
|
|
206
|
+
'.svg': 'image/svg+xml',
|
|
207
|
+
'.gif': 'image/gif'
|
|
208
|
+
};
|
|
209
|
+
res.header('Content-Type', contentTypes[extOriginal] || 'application/octet-stream');
|
|
210
|
+
}
|
|
211
|
+
const optimizedBuffer = await transformer.toBuffer();
|
|
212
|
+
// 3. SALVA NO CACHE PARA A PRÓXIMA VEZ
|
|
213
|
+
// Escreve em background para não bloquear totalmente, mas aguarda para segurança
|
|
214
|
+
try {
|
|
215
|
+
await fs_1.default.promises.writeFile(cachedFilePath, optimizedBuffer);
|
|
216
|
+
}
|
|
217
|
+
catch (writeErr) {
|
|
218
|
+
console_1.default.error('Failed to write image cache:', writeErr);
|
|
219
|
+
}
|
|
220
|
+
res.send(optimizedBuffer);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console_1.default.error('Error optimizing image:', error);
|
|
224
|
+
res.status(500).text('Image optimization failed');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
93
227
|
// Exporta os adapters para uso manual se necessário
|
|
94
228
|
var express_2 = require("./adapters/express");
|
|
95
229
|
Object.defineProperty(exports, "ExpressAdapter", { enumerable: true, get: function () { return express_2.ExpressAdapter; } });
|
|
@@ -332,7 +466,8 @@ function vatts(options) {
|
|
|
332
466
|
genericReq.hotReloadManager = hotReloadManager;
|
|
333
467
|
const { hostname } = req.headers;
|
|
334
468
|
const method = (genericReq.method || 'GET').toUpperCase();
|
|
335
|
-
const
|
|
469
|
+
const urlObj = new URL(genericReq.url, `http://${hostname}:${port}`);
|
|
470
|
+
const pathname = urlObj.pathname;
|
|
336
471
|
if (pathname === types_1.RPC_ENDPOINT && method === 'POST') {
|
|
337
472
|
try {
|
|
338
473
|
const result = await (0, server_1.executeRpc)({
|
|
@@ -352,6 +487,12 @@ function vatts(options) {
|
|
|
352
487
|
if (pathname === '/hweb-hotreload/' && genericReq.headers.upgrade === 'websocket' && hotReloadManager) {
|
|
353
488
|
return;
|
|
354
489
|
}
|
|
490
|
+
// --- SISTEMA DE OTIMIZAÇÃO DE IMAGEM ---
|
|
491
|
+
if (pathname.includes('/_vatts/image')) {
|
|
492
|
+
await handleImageOptimization(genericReq, genericRes, dir);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------
|
|
355
496
|
if (pathname !== '/' && !pathname.startsWith('/api/') && !pathname.startsWith('/.vatts')) {
|
|
356
497
|
const publicDir = path_1.default.join(dir, 'public');
|
|
357
498
|
if (!isSuspiciousPathname(pathname)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vatts",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Vatts.js is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "itsmuzin",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"commander": "^14.0.2",
|
|
81
81
|
"rollup": "^4.55.2",
|
|
82
82
|
"rollup-plugin-esbuild": "^6.2.1",
|
|
83
|
+
"sharp": "^0.34.5",
|
|
83
84
|
"ts-loader": "9.5.4",
|
|
84
85
|
"ts-node": "^10.9.2",
|
|
85
86
|
"typescript": "^5.9.3",
|
|
@@ -94,7 +95,8 @@
|
|
|
94
95
|
"@types/express": "^4.17.25",
|
|
95
96
|
"@types/react": "^19.2.9",
|
|
96
97
|
"@types/react-dom": "^19.2.3",
|
|
97
|
-
"@types/ws": "^8.18.1"
|
|
98
|
+
"@types/ws": "^8.18.1",
|
|
99
|
+
"image-size": "^2.0.2"
|
|
98
100
|
},
|
|
99
101
|
"scripts": {
|
|
100
102
|
"build": "tsc"
|