@synchjs/ewb 1.0.0 → 1.0.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 +76 -196
- package/dist/Components/ServeMemoryStore.d.ts +15 -1
- package/dist/Components/ServeMemoryStore.d.ts.map +1 -1
- package/dist/Components/ServeMemoryStore.js +210 -28
- package/dist/Components/Server.d.ts +12 -1
- package/dist/Components/Server.d.ts.map +1 -1
- package/dist/Components/Server.js +185 -42
- package/dist/Components/UserHandler.d.ts +21 -0
- package/dist/Components/UserHandler.d.ts.map +1 -0
- package/dist/Components/UserHandler.js +9 -0
- package/dist/Decorations/Authorized.d.ts +2 -5
- package/dist/Decorations/Authorized.d.ts.map +1 -1
- package/dist/Decorations/Authorized.js +6 -6
- package/dist/Decorations/Security.d.ts +0 -1
- package/dist/Decorations/Security.d.ts.map +1 -1
- package/dist/Decorations/Security.js +0 -3
- package/dist/Decorations/Serve.d.ts +1 -0
- package/dist/Decorations/Serve.d.ts.map +1 -1
- package/dist/Decorations/Serve.js +20 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +12 -9
|
@@ -1,27 +1,142 @@
|
|
|
1
1
|
import tailwindPlugin from "bun-plugin-tailwind";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { load } from "cheerio";
|
|
2
5
|
export class ServeMemoryStore {
|
|
3
6
|
static _instance;
|
|
4
7
|
_assets = new Map();
|
|
5
8
|
_htmlCache = new Map();
|
|
6
|
-
|
|
9
|
+
_cacheDir = path.join(process.cwd(), ".ebw-cache");
|
|
10
|
+
_devMode = false;
|
|
11
|
+
_watchers = new Map();
|
|
12
|
+
_listeners = [];
|
|
13
|
+
constructor() {
|
|
14
|
+
this.ensureCacheDir();
|
|
15
|
+
}
|
|
16
|
+
ensureCacheDir() {
|
|
17
|
+
if (!fs.existsSync(this._cacheDir)) {
|
|
18
|
+
try {
|
|
19
|
+
fs.mkdirSync(this._cacheDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
// Fallback if we can't create directory
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
clearCache() {
|
|
27
|
+
if (fs.existsSync(this._cacheDir)) {
|
|
28
|
+
try {
|
|
29
|
+
const files = fs.readdirSync(this._cacheDir);
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
fs.unlinkSync(path.join(this._cacheDir, file));
|
|
32
|
+
}
|
|
33
|
+
// Silent clear
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error("[Cache] Error clearing cache:", e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this._htmlCache.clear();
|
|
40
|
+
this._assets.clear();
|
|
41
|
+
}
|
|
7
42
|
static get instance() {
|
|
8
43
|
if (!ServeMemoryStore._instance) {
|
|
9
44
|
ServeMemoryStore._instance = new ServeMemoryStore();
|
|
10
45
|
}
|
|
11
46
|
return ServeMemoryStore._instance;
|
|
12
47
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// We will store with leading slash.
|
|
16
|
-
return this._assets.get(path);
|
|
48
|
+
setDevMode(enabled) {
|
|
49
|
+
this._devMode = enabled;
|
|
17
50
|
}
|
|
18
|
-
|
|
19
|
-
|
|
51
|
+
onRebuild(listener) {
|
|
52
|
+
this._listeners.push(listener);
|
|
53
|
+
}
|
|
54
|
+
notify(data) {
|
|
55
|
+
this._listeners.forEach((l) => l(data));
|
|
56
|
+
}
|
|
57
|
+
getAsset(rawPath) {
|
|
58
|
+
// Basic path traversal protection
|
|
59
|
+
const normalizedPath = path.posix.normalize(rawPath);
|
|
60
|
+
if (normalizedPath.includes(".."))
|
|
61
|
+
return undefined;
|
|
62
|
+
return this._assets.get(normalizedPath);
|
|
63
|
+
}
|
|
64
|
+
getCacheKey(htmlPath, options) {
|
|
65
|
+
return (htmlPath +
|
|
20
66
|
(options.enable ? ":tw" : "") +
|
|
21
|
-
(options.plugins ? ":" + options.plugins.length : "");
|
|
22
|
-
|
|
67
|
+
(options.plugins ? ":" + options.plugins.length : ""));
|
|
68
|
+
}
|
|
69
|
+
async loadFromDisk(cacheKey) {
|
|
70
|
+
if (this._devMode)
|
|
71
|
+
return null; // Skip disk cache in dev mode
|
|
72
|
+
const hash = Bun.hash(cacheKey).toString(16);
|
|
73
|
+
const cacheFile = path.join(this._cacheDir, `${hash}.json`);
|
|
74
|
+
if (fs.existsSync(cacheFile)) {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
|
|
77
|
+
this._htmlCache.set(cacheKey, data.html);
|
|
78
|
+
for (const [webPath, asset] of Object.entries(data.assets)) {
|
|
79
|
+
this._assets.set(webPath, {
|
|
80
|
+
type: asset.type,
|
|
81
|
+
content: Buffer.from(asset.content, "base64"),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return data.html;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error("Error loading cache from disk:", e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
setupWatcher(htmlPath, options) {
|
|
93
|
+
const cacheKey = this.getCacheKey(htmlPath, options);
|
|
94
|
+
if (this._watchers.has(cacheKey))
|
|
95
|
+
return;
|
|
96
|
+
const absolutePath = path.resolve(process.cwd(), htmlPath);
|
|
97
|
+
const watchDir = path.dirname(absolutePath);
|
|
98
|
+
const watcher = fs.watch(watchDir, { recursive: true }, async (event, filename) => {
|
|
99
|
+
if (!filename)
|
|
100
|
+
return;
|
|
101
|
+
// Ignore common noise
|
|
102
|
+
if (filename.includes("node_modules") ||
|
|
103
|
+
filename.includes(".git") ||
|
|
104
|
+
filename.includes(".ebw-cache"))
|
|
105
|
+
return;
|
|
106
|
+
// Silent detect
|
|
107
|
+
// Clear cache for this entry
|
|
108
|
+
this._htmlCache.delete(cacheKey);
|
|
109
|
+
// Clear disk cache by deleting file
|
|
110
|
+
const hash = Bun.hash(cacheKey).toString(16);
|
|
111
|
+
const cacheFile = path.join(this._cacheDir, `${hash}.json`);
|
|
112
|
+
if (fs.existsSync(cacheFile)) {
|
|
113
|
+
fs.unlinkSync(cacheFile);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const newHtml = await this.buildAndCache(htmlPath, options);
|
|
117
|
+
this.notify({ html: newHtml });
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.error("[HMR] Rebuild failed:", e);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
this._watchers.set(cacheKey, watcher);
|
|
124
|
+
}
|
|
125
|
+
async buildAndCache(htmlPath, options = { enable: false, plugins: [] }) {
|
|
126
|
+
const cacheKey = this.getCacheKey(htmlPath, options);
|
|
127
|
+
// 1. Check Memory Cache
|
|
128
|
+
if (!this._devMode && this._htmlCache.has(cacheKey)) {
|
|
23
129
|
return this._htmlCache.get(cacheKey);
|
|
24
130
|
}
|
|
131
|
+
// 2. Check Disk Cache
|
|
132
|
+
const diskHtml = await this.loadFromDisk(cacheKey);
|
|
133
|
+
if (diskHtml) {
|
|
134
|
+
if (this._devMode) {
|
|
135
|
+
this.setupWatcher(htmlPath, options);
|
|
136
|
+
}
|
|
137
|
+
return diskHtml;
|
|
138
|
+
}
|
|
139
|
+
// 3. Perform Build
|
|
25
140
|
try {
|
|
26
141
|
const plugins = [...(options.plugins || [])];
|
|
27
142
|
if (options.enable) {
|
|
@@ -30,9 +145,9 @@ export class ServeMemoryStore {
|
|
|
30
145
|
const build = await Bun.build({
|
|
31
146
|
entrypoints: [htmlPath],
|
|
32
147
|
target: "browser",
|
|
33
|
-
minify:
|
|
34
|
-
naming: "[name]-[hash].[ext]",
|
|
35
|
-
publicPath: "/",
|
|
148
|
+
minify: !this._devMode,
|
|
149
|
+
naming: "[name]-[hash].[ext]",
|
|
150
|
+
publicPath: "/",
|
|
36
151
|
plugins: plugins,
|
|
37
152
|
});
|
|
38
153
|
if (!build.success) {
|
|
@@ -40,33 +155,100 @@ export class ServeMemoryStore {
|
|
|
40
155
|
throw new Error("Build failed");
|
|
41
156
|
}
|
|
42
157
|
let htmlContent = "";
|
|
158
|
+
const currentBuildAssets = {};
|
|
43
159
|
for (const output of build.outputs) {
|
|
44
160
|
const content = await output.arrayBuffer();
|
|
45
|
-
|
|
46
|
-
// output.path is the absolute path if we were writing, or the name.
|
|
47
|
-
// With naming option, output.path usually contains the generated name.
|
|
48
|
-
// For artifacts, we need to map the requested URL to this content.
|
|
49
|
-
// output.kind gives us hint.
|
|
50
|
-
// Parse the relative path (URL) from the output.
|
|
51
|
-
// Since we didn't specify outdir, Bun might give us just the name or path relative to cwd.
|
|
52
|
-
// However, with `publicPath: "/"`, the HTML imports will look like `/foo-hash.js`.
|
|
53
|
-
// We need to store keys as `/foo-hash.js`.
|
|
54
|
-
// Let's rely on the fact that `output.path` usually returns what would be written to disk.
|
|
55
|
-
// We need the filename part.
|
|
161
|
+
const uint8 = new Uint8Array(content);
|
|
56
162
|
const filename = output.path.split(/[/\\]/).pop();
|
|
57
163
|
const webPath = "/" + filename;
|
|
58
164
|
if (output.type === "text/html" || filename?.endsWith(".html")) {
|
|
59
|
-
htmlContent = text;
|
|
165
|
+
htmlContent = await output.text();
|
|
60
166
|
}
|
|
61
167
|
else {
|
|
62
|
-
|
|
63
|
-
this._assets.set(webPath, {
|
|
168
|
+
const asset = {
|
|
64
169
|
type: output.type,
|
|
65
|
-
content:
|
|
66
|
-
}
|
|
170
|
+
content: uint8,
|
|
171
|
+
};
|
|
172
|
+
this._assets.set(webPath, asset);
|
|
173
|
+
currentBuildAssets[webPath] = {
|
|
174
|
+
type: output.type,
|
|
175
|
+
content: Buffer.from(uint8).toString("base64"),
|
|
176
|
+
};
|
|
67
177
|
}
|
|
68
178
|
}
|
|
179
|
+
// Inject HMR/Reload script in dev mode
|
|
180
|
+
if (this._devMode) {
|
|
181
|
+
const reloadScript = `
|
|
182
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
183
|
+
<script id="ebw-hmr-script">
|
|
184
|
+
(function() {
|
|
185
|
+
const socket = io(window.location.origin);
|
|
186
|
+
|
|
187
|
+
socket.on('rebuild', (data) => {
|
|
188
|
+
if (!data || !data.html) return;
|
|
189
|
+
console.log('[HMR] Rebuild detected. Hot swapping scripts...');
|
|
190
|
+
|
|
191
|
+
const parser = new DOMParser();
|
|
192
|
+
const newDoc = parser.parseFromString(data.html, 'text/html');
|
|
193
|
+
const newScripts = Array.from(newDoc.querySelectorAll('script[src]'))
|
|
194
|
+
.filter(s => !s.id && s.getAttribute('src').includes('-')); // Find bundled scripts
|
|
195
|
+
|
|
196
|
+
if (newScripts.length === 0) {
|
|
197
|
+
console.warn('[HMR] No bundled scripts found in rebuild. Falling back to reload.');
|
|
198
|
+
location.reload();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remove old bundled scripts
|
|
203
|
+
const oldScripts = Array.from(document.querySelectorAll('script[src]'))
|
|
204
|
+
.filter(s => !s.id && s.getAttribute('src').includes('-'));
|
|
205
|
+
|
|
206
|
+
oldScripts.forEach(s => s.remove());
|
|
207
|
+
|
|
208
|
+
// Add new scripts
|
|
209
|
+
newScripts.forEach(s => {
|
|
210
|
+
const script = document.createElement('script');
|
|
211
|
+
Array.from(s.attributes).forEach(attr => script.setAttribute(attr.name, attr.value));
|
|
212
|
+
document.body.appendChild(script);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log('[HMR] Hot swap complete!');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
socket.on('reload', () => {
|
|
219
|
+
console.log('[HMR] Hard reload signal received.');
|
|
220
|
+
location.reload();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
socket.on('disconnect', () => {
|
|
224
|
+
console.warn('[HMR] Connection lost. Attempting to reconnect...');
|
|
225
|
+
});
|
|
226
|
+
})();
|
|
227
|
+
</script>
|
|
228
|
+
`;
|
|
229
|
+
// In dev mode, we might want to prevent caching by appending a timestamp to asset links in the HTML
|
|
230
|
+
const timestamp = Date.now();
|
|
231
|
+
htmlContent = htmlContent.replace(/(\.js|\.css)(\?.*)?/g, `$1?t=${timestamp}`);
|
|
232
|
+
const $ = load(htmlContent);
|
|
233
|
+
if ($("body").length > 0) {
|
|
234
|
+
$("body").append(reloadScript);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
$.root().append(reloadScript);
|
|
238
|
+
}
|
|
239
|
+
htmlContent = $.html();
|
|
240
|
+
this.setupWatcher(htmlPath, options);
|
|
241
|
+
}
|
|
242
|
+
// 4. Save to Memory and Disk
|
|
69
243
|
this._htmlCache.set(cacheKey, htmlContent);
|
|
244
|
+
if (!this._devMode) {
|
|
245
|
+
const hash = Bun.hash(cacheKey).toString(16);
|
|
246
|
+
const cacheFile = path.join(this._cacheDir, `${hash}.json`);
|
|
247
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
248
|
+
html: htmlContent,
|
|
249
|
+
assets: currentBuildAssets,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
70
252
|
return htmlContent;
|
|
71
253
|
}
|
|
72
254
|
catch (error) {
|
|
@@ -2,10 +2,12 @@ import { type Request, type Response, type NextFunction } from "express";
|
|
|
2
2
|
import { type HelmetOptions } from "helmet";
|
|
3
3
|
import cors from "cors";
|
|
4
4
|
import { type Options as RateLimitOptions } from "express-rate-limit";
|
|
5
|
+
import { UserHandler } from "./UserHandler";
|
|
5
6
|
export declare class Server {
|
|
6
7
|
private readonly _app;
|
|
7
8
|
private readonly _port;
|
|
8
9
|
private readonly _controllersDir;
|
|
10
|
+
private readonly _viewsDir?;
|
|
9
11
|
readonly _id: string;
|
|
10
12
|
private readonly _enableSwagger;
|
|
11
13
|
private readonly _swaggerPath;
|
|
@@ -14,17 +16,24 @@ export declare class Server {
|
|
|
14
16
|
private readonly _ajv;
|
|
15
17
|
private readonly _securityHandlers;
|
|
16
18
|
private readonly _container?;
|
|
19
|
+
private readonly _roleHandler?;
|
|
20
|
+
private _userHandler?;
|
|
17
21
|
private _serverInstance?;
|
|
22
|
+
private _io?;
|
|
18
23
|
private _controllerCount;
|
|
19
24
|
private _routeCount;
|
|
20
25
|
private _tailwindEnabled;
|
|
26
|
+
private _devMode;
|
|
21
27
|
constructor(options: ServerOptions);
|
|
28
|
+
private setupHmr;
|
|
22
29
|
private log;
|
|
23
30
|
init(): Promise<void>;
|
|
24
31
|
private printStartupMessage;
|
|
25
32
|
close(): void;
|
|
33
|
+
setUserHandler(handler: UserHandler): void;
|
|
34
|
+
private setupAuthRoutes;
|
|
26
35
|
private loadControllers;
|
|
27
|
-
private
|
|
36
|
+
private createRoleMiddleware;
|
|
28
37
|
private createAuthMiddleware;
|
|
29
38
|
private createValidationMiddleware;
|
|
30
39
|
private setupSwagger;
|
|
@@ -36,6 +45,7 @@ export interface ServerOptions {
|
|
|
36
45
|
logging?: boolean;
|
|
37
46
|
id: string;
|
|
38
47
|
controllersDir?: string;
|
|
48
|
+
viewsDir?: string;
|
|
39
49
|
corsOptions?: cors.CorsOptions;
|
|
40
50
|
helmetOptions?: HelmetOptions;
|
|
41
51
|
rateLimitOptions?: Partial<RateLimitOptions>;
|
|
@@ -44,5 +54,6 @@ export interface ServerOptions {
|
|
|
44
54
|
container?: {
|
|
45
55
|
get: (target: any) => any;
|
|
46
56
|
};
|
|
57
|
+
roleHandler?: (req: Request, roles: string[]) => boolean | Promise<boolean>;
|
|
47
58
|
}
|
|
48
59
|
//# sourceMappingURL=Server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Server.d.ts","sourceRoot":"","sources":["../../src/Components/Server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAEd,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;AAKjB,OAAe,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAkB,EAChB,KAAK,OAAO,IAAI,gBAAgB,EACjC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"Server.d.ts","sourceRoot":"","sources":["../../src/Components/Server.ts"],"names":[],"mappings":"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"}
|