frontend-hamroun 1.1.90 → 1.2.0
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/{src/backend → backend}/api-utils.d.ts +2 -2
- package/dist/backend/api-utils.js +135 -0
- package/dist/backend/auth.js +387 -0
- package/dist/{src/backend → backend}/database.d.ts +1 -1
- package/dist/backend/database.js +91 -0
- package/dist/{src/backend → backend}/model.d.ts +2 -2
- package/dist/backend/model.js +176 -0
- package/dist/{src/backend → backend}/router.d.ts +1 -1
- package/dist/backend/router.js +137 -0
- package/dist/backend/server.js +268 -0
- package/dist/batch.js +22 -0
- package/dist/cli/index.js +1 -0
- package/dist/{src/component.d.ts → component.d.ts} +1 -1
- package/dist/component.js +84 -0
- package/dist/components/Counter.js +2 -0
- package/dist/context.js +20 -0
- package/dist/frontend-hamroun.es.js +1680 -0
- package/dist/frontend-hamroun.es.js.map +1 -0
- package/dist/frontend-hamroun.umd.js +2 -0
- package/dist/frontend-hamroun.umd.js.map +1 -0
- package/dist/hooks.js +164 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +52 -355
- package/dist/jsx-runtime/index.d.ts +9 -0
- package/dist/jsx-runtime/index.js +16 -0
- package/dist/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/jsx-runtime/jsx-runtime.js +91 -0
- package/dist/{src/jsx-runtime.d.ts → jsx-runtime.d.ts} +1 -1
- package/dist/jsx-runtime.js +192 -0
- package/dist/renderer.js +51 -0
- package/dist/{src/server-renderer.d.ts → server-renderer.d.ts} +3 -0
- package/dist/server-renderer.js +102 -0
- package/dist/vdom.js +27 -0
- package/package.json +38 -52
- package/scripts/generate.js +134 -0
- package/src/backend/api-utils.ts +178 -0
- package/src/backend/auth.ts +543 -0
- package/src/backend/database.ts +104 -0
- package/src/backend/model.ts +196 -0
- package/src/backend/router.ts +176 -0
- package/src/backend/server.ts +330 -0
- package/src/backend/types.ts +257 -0
- package/src/batch.ts +24 -0
- package/src/cli/index.js +22 -40
- package/src/component.ts +98 -0
- package/src/components/Counter.tsx +4 -0
- package/src/context.ts +32 -0
- package/src/hooks.ts +211 -0
- package/src/index.ts +113 -0
- package/src/jsx-runtime/index.ts +24 -0
- package/src/jsx-runtime/jsx-dev-runtime.ts +0 -0
- package/src/jsx-runtime/jsx-runtime.ts +99 -0
- package/src/jsx-runtime.ts +226 -0
- package/src/renderer.ts +55 -0
- package/src/server-renderer.ts +114 -0
- package/src/types/bcrypt.d.ts +30 -0
- package/src/types/jsonwebtoken.d.ts +55 -0
- package/src/types.d.ts +26 -0
- package/src/types.ts +21 -0
- package/src/vdom.ts +34 -0
- package/templates/basic-app/package.json +17 -15
- package/templates/basic-app/postcss.config.js +1 -0
- package/templates/basic-app/src/App.tsx +65 -0
- package/templates/basic-app/src/api.ts +58 -0
- package/templates/basic-app/src/components/Counter.tsx +26 -0
- package/templates/basic-app/src/components/Header.tsx +9 -0
- package/templates/basic-app/src/components/TodoList.tsx +90 -0
- package/templates/basic-app/src/main.ts +20 -0
- package/templates/basic-app/src/server.ts +99 -0
- package/templates/basic-app/tailwind.config.js +23 -2
- package/bin/cli.js +0 -371
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -139269
- package/dist/index.mjs.map +0 -1
- package/dist/src/index.d.ts +0 -16
- package/dist/test/setupTests.d.ts +0 -4
- /package/dist/{src/backend → backend}/auth.d.ts +0 -0
- /package/dist/{src/backend → backend}/server.d.ts +0 -0
- /package/dist/{src/backend → backend}/types.d.ts +0 -0
- /package/dist/{test/backend.test.d.ts → backend/types.js} +0 -0
- /package/dist/{src/batch.d.ts → batch.d.ts} +0 -0
- /package/dist/{src/cli → cli}/index.d.ts +0 -0
- /package/dist/{src/components → components}/Counter.d.ts +0 -0
- /package/dist/{src/context.d.ts → context.d.ts} +0 -0
- /package/dist/{src/hooks.d.ts → hooks.d.ts} +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-dev-runtime.d.ts +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-runtime.d.ts +0 -0
- /package/dist/{src/renderer.d.ts → renderer.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{test/mockTest.d.ts → types.js} +0 -0
- /package/dist/{src/vdom.d.ts → vdom.d.ts} +0 -0
- /package/{dist/test/mongooseSetup.d.ts → src/cli/index.ts} +0 -0
@@ -0,0 +1,257 @@
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
2
|
+
import { DatabaseConnector } from './database';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Options for configuring a Hamroun server
|
6
|
+
*/
|
7
|
+
export interface HamrounServerOptions {
|
8
|
+
/**
|
9
|
+
* Port to run the server on (default: 3000)
|
10
|
+
*/
|
11
|
+
port?: number;
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Directory to serve static files from (default: 'public')
|
15
|
+
*/
|
16
|
+
staticDir?: string;
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Enable CORS headers (default: true)
|
20
|
+
*/
|
21
|
+
enableCors?: boolean;
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Prefix for API routes (default: '/api')
|
25
|
+
*/
|
26
|
+
apiPrefix?: string;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Enable server-side rendering (default: true)
|
30
|
+
*/
|
31
|
+
ssrEnabled?: boolean;
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Additional Express middleware to apply
|
35
|
+
*/
|
36
|
+
middlewares?: RequestHandler[];
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Enable response compression (default: true)
|
40
|
+
*/
|
41
|
+
enableCompression?: boolean;
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Enable helmet security headers (default: true)
|
45
|
+
*/
|
46
|
+
enableHelmet?: boolean;
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Disable Content Security Policy (default: false)
|
50
|
+
*/
|
51
|
+
disableCSP?: boolean;
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Logging format for Morgan (default: 'dev')
|
55
|
+
*/
|
56
|
+
logFormat?: string | null;
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Trust proxy headers (default: false)
|
60
|
+
*/
|
61
|
+
trustProxy?: boolean | string | number | string[] | number[];
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Static file cache age (default: '1d')
|
65
|
+
*/
|
66
|
+
staticCacheAge?: string;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Show detailed error information in responses (default: false)
|
70
|
+
*/
|
71
|
+
showErrorDetails?: boolean;
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Options for router configuration
|
76
|
+
*/
|
77
|
+
export interface RouterOptions {
|
78
|
+
/**
|
79
|
+
* Prefix to apply to all routes (overrides server apiPrefix)
|
80
|
+
*/
|
81
|
+
prefix?: string;
|
82
|
+
|
83
|
+
/**
|
84
|
+
* Enable authentication for all routes in this router
|
85
|
+
*/
|
86
|
+
requireAuth?: boolean;
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Required role for all routes in this router
|
90
|
+
*/
|
91
|
+
requiredRole?: string | string[];
|
92
|
+
|
93
|
+
/**
|
94
|
+
* Enable rate limiting for this router
|
95
|
+
*/
|
96
|
+
rateLimit?: {
|
97
|
+
windowMs: number;
|
98
|
+
max: number;
|
99
|
+
message?: string;
|
100
|
+
};
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Context provided to route handlers
|
105
|
+
*/
|
106
|
+
export interface RouteContext {
|
107
|
+
req: Request;
|
108
|
+
res: Response;
|
109
|
+
next: NextFunction;
|
110
|
+
params: any;
|
111
|
+
query: any;
|
112
|
+
body: any;
|
113
|
+
user?: any;
|
114
|
+
}
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Pagination options
|
118
|
+
*/
|
119
|
+
export interface PaginationOptions {
|
120
|
+
page: number;
|
121
|
+
limit: number;
|
122
|
+
sort?: string;
|
123
|
+
order?: 'asc' | 'desc';
|
124
|
+
}
|
125
|
+
|
126
|
+
/**
|
127
|
+
* Paginated result
|
128
|
+
*/
|
129
|
+
export interface PaginatedResult<T> {
|
130
|
+
data: T[];
|
131
|
+
pagination: {
|
132
|
+
total: number;
|
133
|
+
totalPages: number;
|
134
|
+
currentPage: number;
|
135
|
+
limit: number;
|
136
|
+
hasNextPage: boolean;
|
137
|
+
hasPrevPage: boolean;
|
138
|
+
};
|
139
|
+
}
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Database connection options
|
143
|
+
*/
|
144
|
+
export interface DatabaseOptions {
|
145
|
+
/**
|
146
|
+
* URI to connect to the database
|
147
|
+
*/
|
148
|
+
uri: string;
|
149
|
+
|
150
|
+
/**
|
151
|
+
* Database name
|
152
|
+
*/
|
153
|
+
name: string;
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Connection options
|
157
|
+
*/
|
158
|
+
options?: Record<string, any>;
|
159
|
+
|
160
|
+
/**
|
161
|
+
* Number of retry attempts (default: 3)
|
162
|
+
*/
|
163
|
+
retryAttempts?: number;
|
164
|
+
|
165
|
+
/**
|
166
|
+
* Retry delay in ms (default: 1000)
|
167
|
+
*/
|
168
|
+
retryDelay?: number;
|
169
|
+
|
170
|
+
/**
|
171
|
+
* Connection timeout in ms (default: 10000)
|
172
|
+
*/
|
173
|
+
connectionTimeout?: number;
|
174
|
+
|
175
|
+
/**
|
176
|
+
* Auto-index creation (default: true)
|
177
|
+
*/
|
178
|
+
autoIndex?: boolean;
|
179
|
+
}
|
180
|
+
|
181
|
+
/**
|
182
|
+
* Data model structure
|
183
|
+
*/
|
184
|
+
export interface Model<T> {
|
185
|
+
/**
|
186
|
+
* Get all documents
|
187
|
+
*/
|
188
|
+
getAll: (options?: PaginationOptions) => Promise<PaginatedResult<T>>;
|
189
|
+
|
190
|
+
/**
|
191
|
+
* Get document by ID
|
192
|
+
*/
|
193
|
+
getById: (id: string) => Promise<T | null>;
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Create a new document
|
197
|
+
*/
|
198
|
+
create: (data: Partial<T>) => Promise<T>;
|
199
|
+
|
200
|
+
/**
|
201
|
+
* Create multiple documents
|
202
|
+
*/
|
203
|
+
createMany: (data: Partial<T>[]) => Promise<T[]>;
|
204
|
+
|
205
|
+
/**
|
206
|
+
* Update a document
|
207
|
+
*/
|
208
|
+
update: (id: string, data: Partial<T>) => Promise<T | null>;
|
209
|
+
|
210
|
+
/**
|
211
|
+
* Delete a document
|
212
|
+
*/
|
213
|
+
delete: (id: string) => Promise<boolean>;
|
214
|
+
|
215
|
+
/**
|
216
|
+
* Find documents by custom query
|
217
|
+
*/
|
218
|
+
find: (query: Record<string, any>, options?: PaginationOptions) => Promise<PaginatedResult<T>>;
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Count documents matching query
|
222
|
+
*/
|
223
|
+
count: (query?: Record<string, any>) => Promise<number>;
|
224
|
+
|
225
|
+
/**
|
226
|
+
* Find one document
|
227
|
+
*/
|
228
|
+
findOne: (query: Record<string, any>) => Promise<T | null>;
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* API response format
|
233
|
+
*/
|
234
|
+
export interface ApiResponse<T = any> {
|
235
|
+
success: boolean;
|
236
|
+
data?: T;
|
237
|
+
error?: string;
|
238
|
+
message?: string;
|
239
|
+
meta?: Record<string, any>;
|
240
|
+
}
|
241
|
+
|
242
|
+
// Extend Express Application interface
|
243
|
+
declare global {
|
244
|
+
namespace Express {
|
245
|
+
interface Application {
|
246
|
+
registerApi: (routePath: string, router: any, options?: RouterOptions) => Application;
|
247
|
+
registerSSR: (routePath: string, component: any, options?: any) => Application;
|
248
|
+
start: (callback?: () => void) => any;
|
249
|
+
connectToDatabase: (dbOptions: DatabaseOptions) => Promise<DatabaseConnector>;
|
250
|
+
}
|
251
|
+
|
252
|
+
interface Request {
|
253
|
+
user?: any;
|
254
|
+
pagination?: PaginationOptions;
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
package/src/batch.ts
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
export let isBatching = false;
|
2
|
+
const queue: Function[] = [];
|
3
|
+
|
4
|
+
export function batchUpdates(fn: Function) {
|
5
|
+
if (isBatching) {
|
6
|
+
queue.push(fn);
|
7
|
+
return;
|
8
|
+
}
|
9
|
+
|
10
|
+
isBatching = true;
|
11
|
+
try {
|
12
|
+
fn();
|
13
|
+
while (queue.length > 0) {
|
14
|
+
const nextFn = queue.shift();
|
15
|
+
nextFn?.();
|
16
|
+
}
|
17
|
+
} finally {
|
18
|
+
isBatching = false;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export function getIsBatching() {
|
23
|
+
return isBatching;
|
24
|
+
}
|
package/src/cli/index.js
CHANGED
@@ -1,60 +1,42 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import fs from 'fs';
|
4
|
+
import path from 'path';
|
5
|
+
import readline from 'readline';
|
6
|
+
import { exec } from 'child_process';
|
7
|
+
import { promisify } from 'util';
|
8
|
+
|
9
|
+
// Promisify fs functions
|
9
10
|
const mkdir = promisify(fs.mkdir);
|
10
|
-
const access = promisify(fs.access);
|
11
11
|
const readdir = promisify(fs.readdir);
|
12
12
|
const writeFile = promisify(fs.writeFile);
|
13
13
|
const readFile = promisify(fs.readFile);
|
14
|
+
const copyFile = promisify(fs.copyFile);
|
15
|
+
const access = promisify(fs.access);
|
16
|
+
const execPromise = promisify(exec);
|
14
17
|
|
18
|
+
// Find the templates directory
|
19
|
+
const TEMPLATES_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'templates');
|
20
|
+
|
21
|
+
// Create readline interface
|
15
22
|
const rl = readline.createInterface({
|
16
23
|
input: process.stdin,
|
17
24
|
output: process.stdout
|
18
25
|
});
|
19
26
|
|
27
|
+
// Promisify question
|
20
28
|
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
|
21
29
|
|
22
|
-
|
23
|
-
const COLORS = {
|
24
|
-
reset: '\x1b[0m',
|
25
|
-
green: '\x1b[32m',
|
26
|
-
yellow: '\x1b[33m',
|
27
|
-
blue: '\x1b[34m',
|
28
|
-
red: '\x1b[31m',
|
29
|
-
cyan: '\x1b[36m'
|
30
|
-
};
|
31
|
-
|
32
|
-
/**
|
33
|
-
* Format colored console log
|
34
|
-
*/
|
30
|
+
// Simple logger
|
35
31
|
const log = {
|
36
|
-
info: (msg) => console.log(
|
37
|
-
success: (msg) => console.log(
|
38
|
-
warn: (msg) => console.log(
|
39
|
-
error: (msg) => console.log(
|
40
|
-
title: (msg) => console.log(`\n${
|
32
|
+
info: (msg) => console.log(`\x1b[36minfo\x1b[0m: ${msg}`),
|
33
|
+
success: (msg) => console.log(`\x1b[32msuccess\x1b[0m: ${msg}`),
|
34
|
+
warn: (msg) => console.log(`\x1b[33mwarning\x1b[0m: ${msg}`),
|
35
|
+
error: (msg) => console.log(`\x1b[31merror\x1b[0m: ${msg}`),
|
36
|
+
title: (msg) => console.log(`\n\x1b[1m${msg}\x1b[0m\n`)
|
41
37
|
};
|
42
38
|
|
43
|
-
|
44
|
-
* Execute shell command
|
45
|
-
*/
|
46
|
-
const execPromise = (cmd, options = {}) => {
|
47
|
-
return new Promise((resolve, reject) => {
|
48
|
-
const process = exec(cmd, options, (error, stdout) => {
|
49
|
-
if (error) {
|
50
|
-
reject(error);
|
51
|
-
return;
|
52
|
-
}
|
53
|
-
resolve(stdout);
|
54
|
-
});
|
55
|
-
});
|
56
|
-
}
|
57
|
-
|
39
|
+
// Rest of the CLI functionality...
|
58
40
|
/**
|
59
41
|
* Copy directory recursively
|
60
42
|
*/
|
package/src/component.ts
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
import { createElement } from './jsx-runtime';
|
2
|
+
|
3
|
+
export class Component {
|
4
|
+
state: any = {};
|
5
|
+
props: any;
|
6
|
+
element: HTMLElement | null = null;
|
7
|
+
private _mounted: boolean = false;
|
8
|
+
|
9
|
+
constructor(props: any = {}) {
|
10
|
+
this.props = props;
|
11
|
+
}
|
12
|
+
|
13
|
+
componentDidMount() {
|
14
|
+
// Hook for after component is mounted
|
15
|
+
}
|
16
|
+
|
17
|
+
async setState(newState: any) {
|
18
|
+
const prevState = { ...this.state };
|
19
|
+
this.state = { ...prevState, ...newState };
|
20
|
+
console.log(`${this.constructor.name} state updated:`, {
|
21
|
+
prev: prevState,
|
22
|
+
next: this.state
|
23
|
+
});
|
24
|
+
|
25
|
+
await Promise.resolve(); // Ensure state is updated before re-render
|
26
|
+
if (this._mounted) {
|
27
|
+
await this.update();
|
28
|
+
} else {
|
29
|
+
await this.update();
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
private _replayEvents(oldElement: HTMLElement, newElement: HTMLElement) {
|
34
|
+
const oldEvents = (oldElement as any).__events || {};
|
35
|
+
Object.entries(oldEvents).forEach(([event, handler]) => {
|
36
|
+
newElement.addEventListener(event as keyof HTMLElementEventMap, handler as EventListener);
|
37
|
+
});
|
38
|
+
(newElement as any).__events = oldEvents;
|
39
|
+
}
|
40
|
+
|
41
|
+
private _deepCloneWithEvents(node: HTMLElement): HTMLElement {
|
42
|
+
const clone = node.cloneNode(false) as HTMLElement;
|
43
|
+
|
44
|
+
// Copy events from original element
|
45
|
+
const events = (node as any).__events || {};
|
46
|
+
(clone as any).__events = events;
|
47
|
+
Object.entries(events).forEach(([event, handler]) => {
|
48
|
+
clone.addEventListener(event as keyof HTMLElementEventMap, handler as EventListener);
|
49
|
+
});
|
50
|
+
|
51
|
+
// Clone children
|
52
|
+
Array.from(node.childNodes).forEach(child => {
|
53
|
+
if (child instanceof HTMLElement) {
|
54
|
+
clone.appendChild(this._deepCloneWithEvents(child));
|
55
|
+
} else {
|
56
|
+
clone.appendChild(child.cloneNode(true));
|
57
|
+
}
|
58
|
+
});
|
59
|
+
|
60
|
+
return clone;
|
61
|
+
}
|
62
|
+
|
63
|
+
async update() {
|
64
|
+
const vdom = this.render();
|
65
|
+
if (!vdom) return document.createTextNode('');
|
66
|
+
|
67
|
+
const rendered = await createElement(vdom);
|
68
|
+
if (rendered instanceof HTMLElement) {
|
69
|
+
return this._updateElement(rendered);
|
70
|
+
}
|
71
|
+
|
72
|
+
const wrapper = document.createElement('div');
|
73
|
+
wrapper.appendChild(rendered);
|
74
|
+
return this._updateElement(wrapper);
|
75
|
+
}
|
76
|
+
|
77
|
+
private async _updateElement(rendered: HTMLElement) {
|
78
|
+
const newElement = this._deepCloneWithEvents(rendered);
|
79
|
+
(newElement as any).__instance = this;
|
80
|
+
|
81
|
+
if (!this.element) {
|
82
|
+
this.element = newElement;
|
83
|
+
if (!this._mounted) {
|
84
|
+
this._mounted = true;
|
85
|
+
queueMicrotask(() => this.componentDidMount());
|
86
|
+
}
|
87
|
+
} else if (this.element.parentNode) {
|
88
|
+
this.element.parentNode.replaceChild(newElement, this.element);
|
89
|
+
this.element = newElement;
|
90
|
+
}
|
91
|
+
|
92
|
+
return this.element;
|
93
|
+
}
|
94
|
+
|
95
|
+
render(): any {
|
96
|
+
throw new Error('Component must implement render() method');
|
97
|
+
}
|
98
|
+
}
|
package/src/context.ts
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
const contexts = new Map<symbol, any>();
|
4
|
+
let currentRender: Function | null = null;
|
5
|
+
|
6
|
+
export interface Context<T> {
|
7
|
+
Provider: (props: { value: T; children?: any }) => any;
|
8
|
+
Consumer: (props: { children: (value: T) => any }) => any;
|
9
|
+
_id: symbol;
|
10
|
+
useSelector: <S>(selector: (state: T) => S) => S;
|
11
|
+
}
|
12
|
+
|
13
|
+
export function createContext<T>(defaultValue: T): Context<T> {
|
14
|
+
const context = {
|
15
|
+
Provider: ({ value, children }: { value: T, children?: any }) => {
|
16
|
+
return children;
|
17
|
+
},
|
18
|
+
Consumer: ({ children }: { children: (value: T) => any }) => {
|
19
|
+
return children(defaultValue);
|
20
|
+
},
|
21
|
+
_id: Symbol(),
|
22
|
+
useSelector: <S>(selector: (state: T) => S) => {
|
23
|
+
return selector(defaultValue);
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
return context;
|
28
|
+
}
|
29
|
+
|
30
|
+
export function useContext<T>(context: any): T {
|
31
|
+
return context;
|
32
|
+
}
|
package/src/hooks.ts
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
import { createElement } from './jsx-runtime';
|
2
|
+
import { batchUpdates, isBatching } from './batch';
|
3
|
+
import { diff } from './vdom';
|
4
|
+
|
5
|
+
let currentRender: number = 0;
|
6
|
+
const states = new Map<number, any[]>();
|
7
|
+
const stateIndices = new Map<number, number>();
|
8
|
+
const effects = new Map<number, Effect[]>();
|
9
|
+
const memos = new Map<number, { value: any; deps: any[] }[]>();
|
10
|
+
const refs = new Map<number, any[]>();
|
11
|
+
|
12
|
+
interface Effect {
|
13
|
+
cleanup?: () => void;
|
14
|
+
deps?: any[];
|
15
|
+
}
|
16
|
+
|
17
|
+
// Add at the top with other declarations
|
18
|
+
let globalRenderCallback: ((element: any, container: HTMLElement) => void) | null = null;
|
19
|
+
let globalContainer: HTMLElement | null = null;
|
20
|
+
let currentElement: any = null;
|
21
|
+
|
22
|
+
const isServer = typeof window === 'undefined';
|
23
|
+
const serverStates = new Map<number, any>();
|
24
|
+
|
25
|
+
export function setRenderCallback(
|
26
|
+
callback: (element: any, container: HTMLElement) => void,
|
27
|
+
element: any,
|
28
|
+
container: HTMLElement
|
29
|
+
) {
|
30
|
+
globalRenderCallback = callback;
|
31
|
+
globalContainer = container;
|
32
|
+
currentElement = element;
|
33
|
+
}
|
34
|
+
|
35
|
+
export function prepareRender() {
|
36
|
+
currentRender++;
|
37
|
+
stateIndices.set(currentRender, 0);
|
38
|
+
return currentRender;
|
39
|
+
}
|
40
|
+
|
41
|
+
export function finishRender() {
|
42
|
+
if (isServer) {
|
43
|
+
serverStates.delete(currentRender);
|
44
|
+
}
|
45
|
+
currentRender = 0;
|
46
|
+
}
|
47
|
+
|
48
|
+
export function useState<T>(initial: T): [T, (value: T | ((prev: T) => T)) => void] {
|
49
|
+
if (!currentRender) {
|
50
|
+
throw new Error('useState must be called within a render');
|
51
|
+
}
|
52
|
+
|
53
|
+
if (isServer) {
|
54
|
+
// Server-side state handling
|
55
|
+
if (!serverStates.has(currentRender)) {
|
56
|
+
serverStates.set(currentRender, new Map());
|
57
|
+
}
|
58
|
+
const componentState = serverStates.get(currentRender)!;
|
59
|
+
const index = stateIndices.get(currentRender) || 0;
|
60
|
+
|
61
|
+
if (!componentState.has(index)) {
|
62
|
+
componentState.set(index, initial);
|
63
|
+
}
|
64
|
+
|
65
|
+
const state = componentState.get(index);
|
66
|
+
const setState = (newValue: T | ((prev: T) => T)) => {
|
67
|
+
// No-op for server-side
|
68
|
+
};
|
69
|
+
|
70
|
+
stateIndices.set(currentRender, index + 1);
|
71
|
+
return [state, setState];
|
72
|
+
}
|
73
|
+
|
74
|
+
if (!states.has(currentRender)) {
|
75
|
+
states.set(currentRender, []);
|
76
|
+
}
|
77
|
+
|
78
|
+
const componentStates = states.get(currentRender)!;
|
79
|
+
const index = stateIndices.get(currentRender)!;
|
80
|
+
|
81
|
+
if (index >= componentStates.length) {
|
82
|
+
componentStates.push(initial);
|
83
|
+
}
|
84
|
+
|
85
|
+
const state = componentStates[index];
|
86
|
+
const setState = (newValue: T | ((prev: T) => T)) => {
|
87
|
+
const nextValue = typeof newValue === 'function'
|
88
|
+
? (newValue as Function)(componentStates[index])
|
89
|
+
: newValue;
|
90
|
+
|
91
|
+
if (componentStates[index] === nextValue) return; // Skip if value hasn't changed
|
92
|
+
|
93
|
+
componentStates[index] = nextValue;
|
94
|
+
|
95
|
+
if (isBatching) {
|
96
|
+
batchUpdates(() => rerender(currentRender));
|
97
|
+
} else {
|
98
|
+
rerender(currentRender);
|
99
|
+
}
|
100
|
+
};
|
101
|
+
|
102
|
+
stateIndices.set(currentRender, index + 1);
|
103
|
+
return [state, setState];
|
104
|
+
}
|
105
|
+
|
106
|
+
export function useEffect(callback: () => (() => void) | void, deps?: any[]) {
|
107
|
+
if (!currentRender) throw new Error('useEffect must be called within a render');
|
108
|
+
|
109
|
+
const effectIndex = stateIndices.get(currentRender)!;
|
110
|
+
|
111
|
+
if (!effects.has(currentRender)) {
|
112
|
+
effects.set(currentRender, []);
|
113
|
+
}
|
114
|
+
|
115
|
+
const componentEffects = effects.get(currentRender)!;
|
116
|
+
const prevEffect = componentEffects[effectIndex];
|
117
|
+
|
118
|
+
// Run effect if deps changed
|
119
|
+
if (!prevEffect || !deps || !prevEffect.deps ||
|
120
|
+
deps.some((dep, i) => dep !== prevEffect.deps![i])) {
|
121
|
+
|
122
|
+
// Cleanup previous effect
|
123
|
+
if (prevEffect?.cleanup) {
|
124
|
+
prevEffect.cleanup();
|
125
|
+
}
|
126
|
+
|
127
|
+
// Schedule new effect
|
128
|
+
queueMicrotask(() => {
|
129
|
+
const cleanup = callback() || undefined;
|
130
|
+
componentEffects[effectIndex] = { cleanup: cleanup, deps };
|
131
|
+
});
|
132
|
+
}
|
133
|
+
|
134
|
+
stateIndices.set(currentRender, effectIndex + 1);
|
135
|
+
}
|
136
|
+
|
137
|
+
export function useMemo<T>(factory: () => T, deps: any[]): T {
|
138
|
+
if (!currentRender) throw new Error('useMemo must be called within a render');
|
139
|
+
|
140
|
+
const memoIndex = stateIndices.get(currentRender)!;
|
141
|
+
|
142
|
+
if (!memos.has(currentRender)) {
|
143
|
+
memos.set(currentRender, []);
|
144
|
+
}
|
145
|
+
|
146
|
+
const componentMemos = memos.get(currentRender)!;
|
147
|
+
const prevMemo = componentMemos[memoIndex];
|
148
|
+
|
149
|
+
if (!prevMemo || (deps && deps.some((dep, i) => !Object.is(dep, prevMemo.deps[i])))) {
|
150
|
+
const value = factory();
|
151
|
+
componentMemos[memoIndex] = { value, deps };
|
152
|
+
stateIndices.set(currentRender, memoIndex + 1);
|
153
|
+
return value;
|
154
|
+
}
|
155
|
+
|
156
|
+
stateIndices.set(currentRender, memoIndex + 1);
|
157
|
+
return prevMemo.value;
|
158
|
+
}
|
159
|
+
|
160
|
+
export function useRef<T>(initial: T) {
|
161
|
+
if (!currentRender) throw new Error('useRef must be called within a render');
|
162
|
+
|
163
|
+
const refIndex = stateIndices.get(currentRender)!;
|
164
|
+
|
165
|
+
if (!refs.has(currentRender)) {
|
166
|
+
refs.set(currentRender, []);
|
167
|
+
}
|
168
|
+
|
169
|
+
const componentRefs = refs.get(currentRender)!;
|
170
|
+
if (refIndex >= componentRefs.length) {
|
171
|
+
// Initialize with an object that has a current property
|
172
|
+
const ref = { current: initial };
|
173
|
+
componentRefs.push(ref);
|
174
|
+
stateIndices.set(currentRender, refIndex + 1);
|
175
|
+
return ref;
|
176
|
+
}
|
177
|
+
|
178
|
+
const ref = componentRefs[refIndex];
|
179
|
+
stateIndices.set(currentRender, refIndex + 1);
|
180
|
+
return ref;
|
181
|
+
}
|
182
|
+
|
183
|
+
// Add a map to track component DOM nodes
|
184
|
+
const componentNodes = new Map<Function, Node>();
|
185
|
+
|
186
|
+
async function rerender(rendererId: number) {
|
187
|
+
try {
|
188
|
+
// Clean up effects
|
189
|
+
const componentEffects = effects.get(rendererId);
|
190
|
+
if (componentEffects) {
|
191
|
+
componentEffects.forEach(effect => {
|
192
|
+
if (effect.cleanup) effect.cleanup();
|
193
|
+
});
|
194
|
+
effects.set(rendererId, []);
|
195
|
+
}
|
196
|
+
|
197
|
+
if (globalRenderCallback && globalContainer && currentElement) {
|
198
|
+
await globalRenderCallback(currentElement, globalContainer);
|
199
|
+
}
|
200
|
+
} catch (error) {
|
201
|
+
console.error('Error during rerender:', error);
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
// Add new hook for error boundaries
|
206
|
+
export function useErrorBoundary(): [Error | null, () => void] {
|
207
|
+
const [error, setError] = useState<Error | null>(null);
|
208
|
+
return [error, () => setError(null)];
|
209
|
+
}
|
210
|
+
|
211
|
+
// Remove withHooks export
|