vatts 1.1.1 → 1.1.3

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.
@@ -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";
@@ -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 pathname = new URL(genericReq.url, `http://${hostname}:${port}`).pathname;
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.1",
3
+ "version": "1.1.3",
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"