hightjs 0.5.0 → 0.5.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/README.md +0 -16
- package/dist/router.js +26 -12
- package/package.json +3 -2
- package/src/adapters/express.ts +87 -0
- package/src/adapters/factory.ts +112 -0
- package/src/adapters/fastify.ts +104 -0
- package/src/adapters/native.ts +234 -0
- package/src/api/console.ts +305 -0
- package/src/api/http.ts +535 -0
- package/src/bin/hightjs.js +252 -0
- package/src/builder.js +631 -0
- package/src/client/DefaultNotFound.tsx +119 -0
- package/src/client/client.ts +25 -0
- package/src/client/clientRouter.ts +153 -0
- package/src/client/entry.client.tsx +526 -0
- package/src/components/Link.tsx +38 -0
- package/src/global/global.ts +171 -0
- package/src/helpers.ts +631 -0
- package/src/hotReload.ts +569 -0
- package/src/index.ts +557 -0
- package/src/loaders.js +53 -0
- package/src/renderer.tsx +421 -0
- package/src/router.ts +744 -0
- package/src/types/framework.ts +58 -0
- package/src/types.ts +258 -0
package/README.md
CHANGED
|
@@ -36,22 +36,6 @@
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## 📦 Installation
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npm install hightjs react@19 react-dom@19
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## 🚀 Quick Start
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npx hight dev
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Visit [http://localhost:3000](http://localhost:3000)
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
39
|
## 📚 Documentation
|
|
56
40
|
|
|
57
41
|
For complete documentation, tutorials, and guides, visit:
|
package/dist/router.js
CHANGED
|
@@ -60,7 +60,7 @@ function clearRequireCache(filePath) {
|
|
|
60
60
|
const resolvedPath = require.resolve(filePath);
|
|
61
61
|
delete require.cache[resolvedPath];
|
|
62
62
|
// Também limpa arquivos temporários relacionados (apenas se existir no cache)
|
|
63
|
-
const tempFile = filePath.replace(/\.(tsx|ts)$/, '.temp.$1');
|
|
63
|
+
const tempFile = filePath.replace(/\.(tsx|ts|jsx|js)$/, '.temp.$1');
|
|
64
64
|
const tempResolvedPath = require.cache[require.resolve(tempFile)];
|
|
65
65
|
if (tempResolvedPath) {
|
|
66
66
|
delete require.cache[require.resolve(tempFile)];
|
|
@@ -110,9 +110,13 @@ function clearFileCache(changedFilePath) {
|
|
|
110
110
|
*/
|
|
111
111
|
function loadLayout(webDir) {
|
|
112
112
|
const layoutPath = path_1.default.join(webDir, 'layout.tsx');
|
|
113
|
-
const
|
|
113
|
+
const layoutPathTs = path_1.default.join(webDir, 'layout.ts');
|
|
114
|
+
const layoutPathJsx = path_1.default.join(webDir, 'layout.jsx');
|
|
115
|
+
const layoutPathJs = path_1.default.join(webDir, 'layout.js');
|
|
114
116
|
const layoutFile = fs_1.default.existsSync(layoutPath) ? layoutPath :
|
|
115
|
-
fs_1.default.existsSync(
|
|
117
|
+
fs_1.default.existsSync(layoutPathTs) ? layoutPathTs :
|
|
118
|
+
fs_1.default.existsSync(layoutPathJsx) ? layoutPathJsx :
|
|
119
|
+
fs_1.default.existsSync(layoutPathJs) ? layoutPathJs : null;
|
|
116
120
|
if (layoutFile) {
|
|
117
121
|
const absolutePath = path_1.default.resolve(layoutFile);
|
|
118
122
|
const componentPath = path_1.default.relative(process.cwd(), layoutFile).replace(/\\/g, '/');
|
|
@@ -123,7 +127,7 @@ function loadLayout(webDir) {
|
|
|
123
127
|
.replace(/import\s+['"][^'"]*\.css['"];?/g, '// CSS import removido para servidor')
|
|
124
128
|
.replace(/import\s+['"][^'"]*\.scss['"];?/g, '// SCSS import removido para servidor')
|
|
125
129
|
.replace(/import\s+['"][^'"]*\.sass['"];?/g, '// SASS import removido para servidor');
|
|
126
|
-
const tempFile = layoutFile.replace(/\.(tsx|ts)$/, '.temp.$1');
|
|
130
|
+
const tempFile = layoutFile.replace(/\.(tsx|ts|jsx|js)$/, '.temp.$1');
|
|
127
131
|
fs_1.default.writeFileSync(tempFile, tempContent);
|
|
128
132
|
// Otimização: limpa cache apenas se existir
|
|
129
133
|
try {
|
|
@@ -181,8 +185,9 @@ function loadRoutes(routesDir) {
|
|
|
181
185
|
scanDirectory(path_1.default.join(dir, entry.name), relativePath);
|
|
182
186
|
}
|
|
183
187
|
else if (entry.isFile()) {
|
|
184
|
-
// Filtra apenas arquivos .ts/.tsx
|
|
185
|
-
if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')
|
|
188
|
+
// Filtra apenas arquivos .ts/.tsx/.js/.jsx
|
|
189
|
+
if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx') ||
|
|
190
|
+
entry.name.endsWith('.js') || entry.name.endsWith('.jsx')) {
|
|
186
191
|
routeFiles.push(relativePath);
|
|
187
192
|
}
|
|
188
193
|
}
|
|
@@ -260,11 +265,15 @@ let loadedMiddlewares = new Map();
|
|
|
260
265
|
*/
|
|
261
266
|
function loadMiddlewareFromDirectory(dir) {
|
|
262
267
|
const middlewares = [];
|
|
263
|
-
// Procura por middleware.ts ou middleware.
|
|
268
|
+
// Procura por middleware.ts, middleware.tsx, middleware.js ou middleware.jsx
|
|
264
269
|
const middlewarePath = path_1.default.join(dir, 'middleware.ts');
|
|
265
270
|
const middlewarePathTsx = path_1.default.join(dir, 'middleware.tsx');
|
|
271
|
+
const middlewarePathJs = path_1.default.join(dir, 'middleware.js');
|
|
272
|
+
const middlewarePathJsx = path_1.default.join(dir, 'middleware.jsx');
|
|
266
273
|
const middlewareFile = fs_1.default.existsSync(middlewarePath) ? middlewarePath :
|
|
267
|
-
fs_1.default.existsSync(middlewarePathTsx) ? middlewarePathTsx :
|
|
274
|
+
fs_1.default.existsSync(middlewarePathTsx) ? middlewarePathTsx :
|
|
275
|
+
fs_1.default.existsSync(middlewarePathJs) ? middlewarePathJs :
|
|
276
|
+
fs_1.default.existsSync(middlewarePathJsx) ? middlewarePathJsx : null;
|
|
268
277
|
if (middlewareFile) {
|
|
269
278
|
try {
|
|
270
279
|
const absolutePath = path_1.default.resolve(middlewareFile);
|
|
@@ -331,8 +340,9 @@ function loadBackendRoutes(backendRoutesDir) {
|
|
|
331
340
|
scanDirectory(path_1.default.join(dir, entry.name), relativePath);
|
|
332
341
|
}
|
|
333
342
|
else if (entry.isFile()) {
|
|
334
|
-
const
|
|
335
|
-
|
|
343
|
+
const isSupported = entry.name.endsWith('.ts') || entry.name.endsWith('.tsx') ||
|
|
344
|
+
entry.name.endsWith('.js') || entry.name.endsWith('.jsx');
|
|
345
|
+
if (!isSupported)
|
|
336
346
|
continue;
|
|
337
347
|
// Identifica middlewares durante o scan
|
|
338
348
|
if (entry.name.startsWith('middleware')) {
|
|
@@ -445,9 +455,13 @@ function findMatchingBackendRoute(pathname, method) {
|
|
|
445
455
|
*/
|
|
446
456
|
function loadNotFound(webDir) {
|
|
447
457
|
const notFoundPath = path_1.default.join(webDir, 'notFound.tsx');
|
|
448
|
-
const
|
|
458
|
+
const notFoundPathTs = path_1.default.join(webDir, 'notFound.ts');
|
|
459
|
+
const notFoundPathJsx = path_1.default.join(webDir, 'notFound.jsx');
|
|
460
|
+
const notFoundPathJs = path_1.default.join(webDir, 'notFound.js');
|
|
449
461
|
const notFoundFile = fs_1.default.existsSync(notFoundPath) ? notFoundPath :
|
|
450
|
-
fs_1.default.existsSync(
|
|
462
|
+
fs_1.default.existsSync(notFoundPathTs) ? notFoundPathTs :
|
|
463
|
+
fs_1.default.existsSync(notFoundPathJsx) ? notFoundPathJsx :
|
|
464
|
+
fs_1.default.existsSync(notFoundPathJs) ? notFoundPathJs : null;
|
|
451
465
|
if (notFoundFile) {
|
|
452
466
|
const absolutePath = path_1.default.resolve(notFoundFile);
|
|
453
467
|
const componentPath = path_1.default.relative(process.cwd(), notFoundFile).replace(/\\/g, '/');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hightjs",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "HightJS 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
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"README.md",
|
|
19
|
-
"dist/global/global.d.ts"
|
|
19
|
+
"dist/global/global.d.ts",
|
|
20
|
+
"src"
|
|
20
21
|
],
|
|
21
22
|
"bin": {
|
|
22
23
|
"hight": "./dist/bin/hightjs.js"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the HightJS Project.
|
|
3
|
+
* Copyright (c) 2025 itsmuzin
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { Request as ExpressRequest, Response as ExpressResponse } from 'express';
|
|
18
|
+
import { GenericRequest, GenericResponse, FrameworkAdapter, CookieOptions } from '../types/framework';
|
|
19
|
+
|
|
20
|
+
export class ExpressAdapter implements FrameworkAdapter {
|
|
21
|
+
type = 'express' as const;
|
|
22
|
+
|
|
23
|
+
parseRequest(req: ExpressRequest): GenericRequest {
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
method: req.method,
|
|
27
|
+
url: req.url,
|
|
28
|
+
headers: req.headers as Record<string, string | string[]>,
|
|
29
|
+
body: req.body,
|
|
30
|
+
query: req.query as Record<string, any>,
|
|
31
|
+
params: req.params,
|
|
32
|
+
cookies: req.cookies || {},
|
|
33
|
+
raw: req,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
createResponse(res: ExpressResponse): GenericResponse {
|
|
38
|
+
return new ExpressResponseWrapper(res);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ExpressResponseWrapper implements GenericResponse {
|
|
43
|
+
constructor(private res: ExpressResponse) {}
|
|
44
|
+
|
|
45
|
+
get raw() {
|
|
46
|
+
return this.res;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
status(code: number): GenericResponse {
|
|
50
|
+
this.res.status(code);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
header(name: string, value: string): GenericResponse {
|
|
55
|
+
this.res.setHeader(name, value);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cookie(name: string, value: string, options?: CookieOptions): GenericResponse {
|
|
60
|
+
this.res.cookie(name, value, options || {});
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearCookie(name: string, options?: CookieOptions): GenericResponse {
|
|
65
|
+
// Filter out the deprecated 'expires' option to avoid Express deprecation warning
|
|
66
|
+
const { expires, ...filteredOptions } = options || {};
|
|
67
|
+
this.res.clearCookie(name, filteredOptions);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
json(data: any): void {
|
|
72
|
+
this.res.json(data);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
text(data: string): void {
|
|
76
|
+
this.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
77
|
+
this.res.send(data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
send(data: any): void {
|
|
81
|
+
this.res.send(data);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
redirect(url: string): void {
|
|
85
|
+
this.res.redirect(url);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the HightJS Project.
|
|
3
|
+
* Copyright (c) 2025 itsmuzin
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { FrameworkAdapter } from '../types/framework';
|
|
18
|
+
import { ExpressAdapter } from './express';
|
|
19
|
+
import { FastifyAdapter } from './fastify';
|
|
20
|
+
import { NativeAdapter } from './native';
|
|
21
|
+
import Console, { Colors} from "../api/console"
|
|
22
|
+
/**
|
|
23
|
+
* Factory para criar o adapter correto baseado no framework detectado
|
|
24
|
+
*/
|
|
25
|
+
export class FrameworkAdapterFactory {
|
|
26
|
+
private static adapter: FrameworkAdapter | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detecta automaticamente o framework baseado na requisição/resposta
|
|
30
|
+
*/
|
|
31
|
+
static detectFramework(req: any, res: any): FrameworkAdapter {
|
|
32
|
+
// Se já detectamos antes, retorna o mesmo adapter
|
|
33
|
+
if (this.adapter) {
|
|
34
|
+
return this.adapter;
|
|
35
|
+
}
|
|
36
|
+
const msg = Console.dynamicLine(` ${Colors.FgYellow}● ${Colors.Reset}Detecting web framework...`);
|
|
37
|
+
|
|
38
|
+
// Detecta Express
|
|
39
|
+
if (req.app && req.route && res.locals !== undefined) {
|
|
40
|
+
msg.end(` ${Colors.FgGreen}● ${Colors.Reset}Framework detected: Express`);
|
|
41
|
+
this.adapter = new ExpressAdapter();
|
|
42
|
+
return this.adapter;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Detecta Fastify
|
|
46
|
+
if (req.server && req.routerPath !== undefined && res.request) {
|
|
47
|
+
msg.end(` ${Colors.FgGreen}● ${Colors.Reset}Framework detected: Fastify`);
|
|
48
|
+
this.adapter = new FastifyAdapter();
|
|
49
|
+
return this.adapter;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Detecta HTTP nativo do Node.js
|
|
53
|
+
if (req.method !== undefined && req.url !== undefined && req.headers !== undefined &&
|
|
54
|
+
res.statusCode !== undefined && res.setHeader !== undefined && res.end !== undefined) {
|
|
55
|
+
msg.end(` ${Colors.FgGreen}● ${Colors.Reset}Framework detected: HightJS Native (HTTP)`);
|
|
56
|
+
this.adapter = new NativeAdapter();
|
|
57
|
+
return this.adapter;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback mais específico para Express
|
|
61
|
+
if (res.status && res.send && res.json && res.cookie) {
|
|
62
|
+
msg.end(` ${Colors.FgGreen}● ${Colors.Reset}Framework detected: Express (fallback)`);
|
|
63
|
+
this.adapter = new ExpressAdapter();
|
|
64
|
+
return this.adapter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback mais específico para Fastify
|
|
68
|
+
if (res.code && res.send && res.type && res.setCookie) {
|
|
69
|
+
msg.end(` ${Colors.FgGreen}● ${Colors.Reset}Framework detected: Fastify (fallback)`);
|
|
70
|
+
this.adapter = new FastifyAdapter();
|
|
71
|
+
return this.adapter;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default para HightJS Native se não conseguir detectar
|
|
75
|
+
msg.end(` ${Colors.FgYellow}● ${Colors.Reset}Unable to detect framework. Using HightJS Native as default.`);
|
|
76
|
+
this.adapter = new NativeAdapter();
|
|
77
|
+
return this.adapter;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Força o uso de um framework específico
|
|
82
|
+
*/
|
|
83
|
+
static setFramework(framework: 'express' | 'fastify' | 'native'): void {
|
|
84
|
+
switch (framework) {
|
|
85
|
+
case 'express':
|
|
86
|
+
this.adapter = new ExpressAdapter();
|
|
87
|
+
break;
|
|
88
|
+
case 'fastify':
|
|
89
|
+
this.adapter = new FastifyAdapter();
|
|
90
|
+
break;
|
|
91
|
+
case 'native':
|
|
92
|
+
this.adapter = new NativeAdapter();
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unsupported framework: ${framework}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset do adapter (útil para testes)
|
|
101
|
+
*/
|
|
102
|
+
static reset(): void {
|
|
103
|
+
this.adapter = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Retorna o adapter atual (se já foi detectado)
|
|
108
|
+
*/
|
|
109
|
+
static getCurrentAdapter(): FrameworkAdapter | null {
|
|
110
|
+
return this.adapter;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the HightJS Project.
|
|
3
|
+
* Copyright (c) 2025 itsmuzin
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
// Tipos para Fastify (sem import direto para evitar dependência obrigatória)
|
|
18
|
+
interface FastifyRequest {
|
|
19
|
+
method: string;
|
|
20
|
+
url: string;
|
|
21
|
+
headers: Record<string, string | string[]>;
|
|
22
|
+
body?: any;
|
|
23
|
+
query?: Record<string, any>;
|
|
24
|
+
params?: Record<string, string>;
|
|
25
|
+
cookies?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface FastifyReply {
|
|
29
|
+
status(code: number): FastifyReply;
|
|
30
|
+
header(name: string, value: string): FastifyReply;
|
|
31
|
+
setCookie(name: string, value: string, options?: any): FastifyReply;
|
|
32
|
+
clearCookie(name: string, options?: any): FastifyReply;
|
|
33
|
+
type(contentType: string): FastifyReply;
|
|
34
|
+
send(data: any): void;
|
|
35
|
+
redirect(url: string): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
import { GenericRequest, GenericResponse, FrameworkAdapter, CookieOptions } from '../types/framework';
|
|
39
|
+
|
|
40
|
+
export class FastifyAdapter implements FrameworkAdapter {
|
|
41
|
+
type = 'fastify' as const;
|
|
42
|
+
|
|
43
|
+
parseRequest(req: FastifyRequest): GenericRequest {
|
|
44
|
+
return {
|
|
45
|
+
method: req.method,
|
|
46
|
+
url: req.url,
|
|
47
|
+
headers: req.headers as Record<string, string | string[]>,
|
|
48
|
+
body: req.body,
|
|
49
|
+
query: req.query as Record<string, any>,
|
|
50
|
+
params: req.params as Record<string, string>,
|
|
51
|
+
cookies: req.cookies || {},
|
|
52
|
+
raw: req
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createResponse(reply: FastifyReply): GenericResponse {
|
|
57
|
+
return new FastifyResponseWrapper(reply);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class FastifyResponseWrapper implements GenericResponse {
|
|
62
|
+
constructor(private reply: FastifyReply) {}
|
|
63
|
+
|
|
64
|
+
get raw() {
|
|
65
|
+
return this.reply;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
status(code: number): GenericResponse {
|
|
69
|
+
this.reply.status(code);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
header(name: string, value: string): GenericResponse {
|
|
74
|
+
this.reply.header(name, value);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cookie(name: string, value: string, options?: CookieOptions): GenericResponse {
|
|
79
|
+
this.reply.setCookie(name, value, options);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clearCookie(name: string, options?: CookieOptions): GenericResponse {
|
|
84
|
+
this.reply.clearCookie(name, options);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
json(data: any): void {
|
|
89
|
+
this.reply.send(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
text(data: string): void {
|
|
93
|
+
this.reply.type('text/plain; charset=utf-8');
|
|
94
|
+
this.reply.send(data);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
send(data: any): void {
|
|
98
|
+
this.reply.send(data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
redirect(url: string): void {
|
|
102
|
+
this.reply.redirect(url);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the HightJS Project.
|
|
3
|
+
* Copyright (c) 2025 itsmuzin
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
18
|
+
import { GenericRequest, GenericResponse, FrameworkAdapter, CookieOptions } from '../types/framework';
|
|
19
|
+
import { parse as parseUrl } from 'url';
|
|
20
|
+
|
|
21
|
+
// --- Funções Auxiliares de Segurança ---
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Remove caracteres de quebra de linha (\r, \n) de uma string para prevenir
|
|
25
|
+
* ataques de HTTP Header Injection (CRLF Injection).
|
|
26
|
+
* @param value O valor a ser sanitizado.
|
|
27
|
+
* @returns A string sanitizada.
|
|
28
|
+
*/
|
|
29
|
+
function sanitizeHeaderValue(value: string | number | boolean): string {
|
|
30
|
+
return String(value).replace(/[\r\n]/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Valida se o nome de um cookie contém apenas caracteres permitidos pela RFC 6265.
|
|
35
|
+
* Isso previne a criação de cookies com nomes inválidos ou maliciosos.
|
|
36
|
+
* @param name O nome do cookie a ser validado.
|
|
37
|
+
* @returns `true` se o nome for válido, `false` caso contrário.
|
|
38
|
+
*/
|
|
39
|
+
function isValidCookieName(name: string): boolean {
|
|
40
|
+
// A RFC 6265 define 'token' como 1 ou mais caracteres que não são controle nem separadores.
|
|
41
|
+
// Separadores: ( ) < > @ , ; : \ " / [ ] ? = { }
|
|
42
|
+
const validCookieNameRegex = /^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$/;
|
|
43
|
+
return validCookieNameRegex.test(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export class NativeAdapter implements FrameworkAdapter {
|
|
48
|
+
type = 'native' as const;
|
|
49
|
+
|
|
50
|
+
parseRequest(req: IncomingMessage): GenericRequest {
|
|
51
|
+
const url = parseUrl(req.url || '', true);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
method: req.method || 'GET',
|
|
55
|
+
url: req.url || '/',
|
|
56
|
+
headers: req.headers as Record<string, string | string[]>,
|
|
57
|
+
// Adicionado fallback para null para maior segurança caso o body parser não tenha rodado.
|
|
58
|
+
body: (req as any).body ?? null,
|
|
59
|
+
// Tipo mais específico para a query.
|
|
60
|
+
query: url.query as Record<string, string | string[]>,
|
|
61
|
+
params: {}, // Será preenchido pelo roteador
|
|
62
|
+
cookies: this.parseCookies(req.headers.cookie || ''),
|
|
63
|
+
raw: req
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createResponse(res: ServerResponse): GenericResponse {
|
|
68
|
+
return new NativeResponseWrapper(res);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private parseCookies(cookieHeader: string): Record<string, string> {
|
|
72
|
+
const cookies: Record<string, string> = {};
|
|
73
|
+
|
|
74
|
+
if (!cookieHeader) return cookies;
|
|
75
|
+
|
|
76
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
77
|
+
const [name, ...rest] = cookie.trim().split('=');
|
|
78
|
+
if (name && rest.length > 0) {
|
|
79
|
+
try {
|
|
80
|
+
// Tenta decodificar o valor do cookie.
|
|
81
|
+
cookies[name] = decodeURIComponent(rest.join('='));
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Prevenção de crash: Ignora cookies com valores malformados (e.g., URI inválida).
|
|
84
|
+
console.error(`Warning: Malformed cookie with name "${name}" was ignored.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return cookies;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class NativeResponseWrapper implements GenericResponse {
|
|
94
|
+
private statusCode = 200;
|
|
95
|
+
private headers: Record<string, string | number> = {};
|
|
96
|
+
private cookiesToSet: string[] = []; // Array para lidar corretamente com múltiplos cookies.
|
|
97
|
+
private finished = false;
|
|
98
|
+
|
|
99
|
+
constructor(private res: ServerResponse) {}
|
|
100
|
+
|
|
101
|
+
get raw() {
|
|
102
|
+
return this.res;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
status(code: number): GenericResponse {
|
|
106
|
+
this.statusCode = code;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
header(name: string, value: string): GenericResponse {
|
|
111
|
+
// Medida de segurança CRÍTICA: Previne HTTP Header Injection (CRLF Injection).
|
|
112
|
+
// Sanitiza tanto o nome quanto o valor do header para remover quebras de linha.
|
|
113
|
+
const sanitizedName = sanitizeHeaderValue(name);
|
|
114
|
+
const sanitizedValue = sanitizeHeaderValue(value);
|
|
115
|
+
|
|
116
|
+
if (name !== sanitizedName || String(value) !== sanitizedValue) {
|
|
117
|
+
console.warn(`Warning: Potential HTTP Header Injection attempt detected and sanitized. Original header: "${name}"`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
this.headers[sanitizedName] = sanitizedValue;
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cookie(name: string, value: string, options?: CookieOptions): GenericResponse {
|
|
126
|
+
// Medida de segurança: Valida o nome do cookie.
|
|
127
|
+
if (!isValidCookieName(name)) {
|
|
128
|
+
console.error(`Error: Invalid cookie name "${name}". The cookie will not be set.`);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let cookieString = `${name}=${encodeURIComponent(value)}`;
|
|
133
|
+
|
|
134
|
+
if (options) {
|
|
135
|
+
// Sanitiza as opções que são strings para prevenir Header Injection.
|
|
136
|
+
if (options.domain) cookieString += `; Domain=${sanitizeHeaderValue(options.domain)}`;
|
|
137
|
+
if (options.path) cookieString += `; Path=${sanitizeHeaderValue(options.path)}`;
|
|
138
|
+
if (options.expires) cookieString += `; Expires=${options.expires.toUTCString()}`;
|
|
139
|
+
if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;
|
|
140
|
+
if (options.httpOnly) cookieString += '; HttpOnly';
|
|
141
|
+
if (options.secure) cookieString += '; Secure';
|
|
142
|
+
if (options.sameSite) {
|
|
143
|
+
const sameSiteValue = typeof options.sameSite === 'boolean' ? 'Strict' : options.sameSite;
|
|
144
|
+
cookieString += `; SameSite=${sanitizeHeaderValue(sameSiteValue)}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.cookiesToSet.push(cookieString);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
clearCookie(name: string, options?: CookieOptions): GenericResponse {
|
|
153
|
+
const clearOptions = { ...options, expires: new Date(0), maxAge: 0 };
|
|
154
|
+
return this.cookie(name, '', clearOptions);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private writeHeaders(): void {
|
|
158
|
+
if (this.finished) return;
|
|
159
|
+
|
|
160
|
+
this.res.statusCode = this.statusCode;
|
|
161
|
+
|
|
162
|
+
Object.entries(this.headers).forEach(([name, value]) => {
|
|
163
|
+
this.res.setHeader(name, value);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// CORREÇÃO: Envia múltiplos cookies corretamente como headers 'Set-Cookie' separados.
|
|
167
|
+
// O método antigo de juntar com vírgula estava incorreto.
|
|
168
|
+
if (this.cookiesToSet.length > 0) {
|
|
169
|
+
this.res.setHeader('Set-Cookie', this.cookiesToSet);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
json(data: any): void {
|
|
174
|
+
if (this.finished) return;
|
|
175
|
+
|
|
176
|
+
this.header('Content-Type', 'application/json; charset=utf-8');
|
|
177
|
+
this.writeHeaders();
|
|
178
|
+
|
|
179
|
+
const jsonString = JSON.stringify(data);
|
|
180
|
+
this.res.end(jsonString);
|
|
181
|
+
this.finished = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
text(data: string): void {
|
|
185
|
+
if (this.finished) return;
|
|
186
|
+
|
|
187
|
+
this.header('Content-Type', 'text/plain; charset=utf-8');
|
|
188
|
+
this.writeHeaders();
|
|
189
|
+
|
|
190
|
+
this.res.end(data);
|
|
191
|
+
this.finished = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
send(data: any): void {
|
|
195
|
+
if (this.finished) return;
|
|
196
|
+
|
|
197
|
+
const existingContentType = this.headers['Content-Type'];
|
|
198
|
+
|
|
199
|
+
if (typeof data === 'string') {
|
|
200
|
+
if (!existingContentType) {
|
|
201
|
+
this.header('Content-Type', 'text/plain; charset=utf-8');
|
|
202
|
+
}
|
|
203
|
+
this.writeHeaders();
|
|
204
|
+
this.res.end(data);
|
|
205
|
+
} else if (Buffer.isBuffer(data)) {
|
|
206
|
+
this.writeHeaders();
|
|
207
|
+
this.res.end(data);
|
|
208
|
+
} else if (data !== null && typeof data === 'object') {
|
|
209
|
+
this.json(data); // Reutiliza o método json para consistência
|
|
210
|
+
return; // O método json já finaliza a resposta
|
|
211
|
+
} else {
|
|
212
|
+
if (!existingContentType) {
|
|
213
|
+
this.header('Content-Type', 'text/plain; charset=utf-8');
|
|
214
|
+
}
|
|
215
|
+
this.writeHeaders();
|
|
216
|
+
this.res.end(String(data));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.finished = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
redirect(url: string): void {
|
|
223
|
+
if (this.finished) return;
|
|
224
|
+
|
|
225
|
+
this.status(302);
|
|
226
|
+
// A sanitização no método .header() previne que um URL manipulado
|
|
227
|
+
// cause um ataque de Open Redirect via Header Injection.
|
|
228
|
+
this.header('Location', url);
|
|
229
|
+
this.writeHeaders();
|
|
230
|
+
|
|
231
|
+
this.res.end();
|
|
232
|
+
this.finished = true;
|
|
233
|
+
}
|
|
234
|
+
}
|