@teamvelix/velix-core 5.2.8 → 5.3.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/index.js CHANGED
@@ -1,14 +1,25 @@
1
1
  import fs from 'fs';
2
- import path2 from 'path';
2
+ import path4 from 'path';
3
3
  import crypto from 'crypto';
4
4
  import { pathToFileURL } from 'url';
5
5
  import { z } from 'zod';
6
6
  import pc from 'picocolors';
7
+ import { LRUCache } from 'lru-cache';
7
8
  import { glob } from 'glob';
8
9
  import { WebSocketServer } from 'ws';
9
10
  import chokidar from 'chokidar';
10
11
 
11
12
  // src/router/index.ts
13
+
14
+ // src/router/types.ts
15
+ var RouteType = {
16
+ PAGE: "page",
17
+ API: "api",
18
+ LAYOUT: "layout",
19
+ LOADING: "loading",
20
+ ERROR: "error",
21
+ NOT_FOUND: "not-found"
22
+ };
12
23
  function generateHash(content) {
13
24
  return crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
14
25
  }
@@ -26,7 +37,7 @@ function findFiles(dir, pattern, files = []) {
26
37
  if (!fs.existsSync(dir)) return files;
27
38
  const entries = fs.readdirSync(dir, { withFileTypes: true });
28
39
  for (const entry of entries) {
29
- const fullPath = path2.join(dir, entry.name);
40
+ const fullPath = path4.join(dir, entry.name);
30
41
  if (entry.isDirectory()) findFiles(fullPath, pattern, files);
31
42
  else if (pattern.test(entry.name)) files.push(fullPath);
32
43
  }
@@ -43,8 +54,8 @@ function copyDir(src, dest) {
43
54
  ensureDir(dest);
44
55
  const entries = fs.readdirSync(src, { withFileTypes: true });
45
56
  for (const entry of entries) {
46
- const srcPath = path2.join(src, entry.name);
47
- const destPath = path2.join(dest, entry.name);
57
+ const srcPath = path4.join(src, entry.name);
58
+ const destPath = path4.join(dest, entry.name);
48
59
  if (entry.isDirectory()) copyDir(srcPath, destPath);
49
60
  else fs.copyFileSync(srcPath, destPath);
50
61
  }
@@ -107,40 +118,29 @@ function isIsland(filePath) {
107
118
  }
108
119
  }
109
120
 
110
- // src/router/index.ts
111
- var RouteType = {
112
- PAGE: "page",
113
- API: "api",
114
- LAYOUT: "layout",
115
- LOADING: "loading",
116
- ERROR: "error",
117
- NOT_FOUND: "not-found"
118
- };
119
- function buildRouteTree(appDir) {
120
- const projectRoot = path2.dirname(appDir);
121
- const routes = {
122
- pages: [],
123
- api: [],
124
- layouts: /* @__PURE__ */ new Map(),
125
- tree: { children: {}, routes: [] },
126
- appRoutes: []
127
- };
128
- if (fs.existsSync(appDir)) {
129
- scanAppDirectory(appDir, appDir, routes);
130
- }
131
- const serverApiDir = path2.join(projectRoot, "server", "api");
132
- if (fs.existsSync(serverApiDir)) {
133
- scanApiDirectory(serverApiDir, serverApiDir, routes);
121
+ // src/router/parser.ts
122
+ function createRoutePattern(routePath) {
123
+ let pattern = routePath.replace(/\*[^/]*/g, "(.*)").replace(/:[^/]+/g, "([^/]+)").replace(/\//g, "\\/");
124
+ return new RegExp(`^${pattern}$`);
125
+ }
126
+ function extractParams(routePath, match) {
127
+ const params = {};
128
+ const paramNames = [];
129
+ const paramRegex = /:([^/]+)|\*([^/]*)/g;
130
+ let paramMatch;
131
+ while ((paramMatch = paramRegex.exec(routePath)) !== null) {
132
+ paramNames.push(paramMatch[1] || paramMatch[2] || "splat");
134
133
  }
135
- const rootLayoutTsx = path2.join(appDir, "layout.tsx");
136
- const rootLayoutJsx = path2.join(appDir, "layout.jsx");
137
- if (fs.existsSync(rootLayoutTsx)) routes.rootLayout = rootLayoutTsx;
138
- else if (fs.existsSync(rootLayoutJsx)) routes.rootLayout = rootLayoutJsx;
139
- routes.tree = buildTree(routes.appRoutes);
140
- return routes;
134
+ paramNames.forEach((name, index) => {
135
+ params[name] = match[index + 1];
136
+ });
137
+ return params;
141
138
  }
142
- function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], parentLayout = null, parentMiddleware = null) {
143
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
139
+
140
+ // src/router/scanner.ts
141
+ async function scanAppDirectory(baseDir, currentDir, parentSegments = [], parentLayout = null, parentMiddleware = null) {
142
+ const routes = [];
143
+ const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
144
144
  const specialFiles = {
145
145
  page: null,
146
146
  layout: null,
@@ -153,8 +153,8 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
153
153
  for (const entry of entries) {
154
154
  if (entry.isFile()) {
155
155
  const name = entry.name.replace(/\.(jsx|js|tsx|ts)$/, "");
156
- const fullPath = path2.join(currentDir, entry.name);
157
- const ext = path2.extname(entry.name);
156
+ const fullPath = path4.join(currentDir, entry.name);
157
+ const ext = path4.extname(entry.name);
158
158
  if (![".tsx", ".jsx", ".ts", ".js"].includes(ext)) continue;
159
159
  if (name === "page") specialFiles.page = fullPath;
160
160
  if (name === "layout") specialFiles.layout = fullPath;
@@ -172,7 +172,7 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
172
172
  segmentName = ":" + paramName;
173
173
  }
174
174
  const routePath = "/" + [...parentSegments, segmentName].join("/");
175
- routes.appRoutes.push({
175
+ routes.push({
176
176
  type: RouteType.PAGE,
177
177
  path: routePath.replace(/\/+/g, "/"),
178
178
  filePath: fullPath,
@@ -193,7 +193,7 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
193
193
  }
194
194
  if (specialFiles.page) {
195
195
  const routePath = "/" + parentSegments.join("/") || "/";
196
- routes.appRoutes.push({
196
+ routes.push({
197
197
  type: RouteType.PAGE,
198
198
  path: routePath.replace(/\/+/g, "/") || "/",
199
199
  filePath: specialFiles.page,
@@ -210,78 +210,58 @@ function scanAppDirectory(baseDir, currentDir, routes, parentSegments = [], pare
210
210
  isIsland: isIsland(specialFiles.page)
211
211
  });
212
212
  }
213
- for (const entry of entries) {
214
- if (entry.isDirectory()) {
215
- const fullPath = path2.join(currentDir, entry.name);
216
- if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
217
- const isGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
218
- let segmentName = entry.name;
219
- if (entry.name.startsWith("[") && entry.name.endsWith("]")) {
220
- segmentName = ":" + entry.name.slice(1, -1);
221
- if (entry.name.startsWith("[...")) {
222
- segmentName = "*" + entry.name.slice(4, -1);
223
- }
224
- if (entry.name.startsWith("[[...")) {
225
- segmentName = "*" + entry.name.slice(5, -2);
226
- }
213
+ const ignoredDirs = ["node_modules", ".velix", "dist"];
214
+ const dirPromises = entries.filter(
215
+ (entry) => entry.isDirectory() && !entry.name.startsWith("_") && !entry.name.startsWith(".") && !ignoredDirs.includes(entry.name)
216
+ ).map(async (entry) => {
217
+ const fullPath = path4.join(currentDir, entry.name);
218
+ const isGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
219
+ let segmentName = entry.name;
220
+ if (entry.name.startsWith("[") && entry.name.endsWith("]")) {
221
+ segmentName = ":" + entry.name.slice(1, -1);
222
+ if (entry.name.startsWith("[...")) {
223
+ segmentName = "*" + entry.name.slice(4, -1);
224
+ }
225
+ if (entry.name.startsWith("[[...")) {
226
+ segmentName = "*" + entry.name.slice(5, -2);
227
227
  }
228
- const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
229
- const newLayout = specialFiles.layout || parentLayout;
230
- const newMiddleware = specialFiles.middleware || parentMiddleware;
231
- scanAppDirectory(baseDir, fullPath, routes, newSegments, newLayout, newMiddleware);
232
228
  }
233
- }
234
- }
235
- function scanApiDirectory(baseDir, currentDir, routes, parentSegments = []) {
236
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
237
- for (const entry of entries) {
238
- const fullPath = path2.join(currentDir, entry.name);
229
+ const newSegments = isGroup ? parentSegments : [...parentSegments, segmentName];
230
+ const newLayout = specialFiles.layout || parentLayout;
231
+ const newMiddleware = specialFiles.middleware || parentMiddleware;
232
+ return scanAppDirectory(baseDir, fullPath, newSegments, newLayout, newMiddleware);
233
+ });
234
+ const subRoutes = await Promise.all(dirPromises);
235
+ return routes.concat(...subRoutes);
236
+ }
237
+ async function scanApiDirectory(baseDir, currentDir, parentSegments = []) {
238
+ const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
239
+ const routes = [];
240
+ const promises = entries.map(async (entry) => {
241
+ const fullPath = path4.join(currentDir, entry.name);
239
242
  if (entry.isDirectory()) {
240
- scanApiDirectory(baseDir, fullPath, routes, [...parentSegments, entry.name]);
243
+ return scanApiDirectory(baseDir, fullPath, [...parentSegments, entry.name]);
241
244
  } else if (entry.isFile()) {
242
- const ext = path2.extname(entry.name);
243
- if (![".ts", ".js"].includes(ext)) continue;
244
- const baseName = path2.basename(entry.name, ext);
245
+ const ext = path4.extname(entry.name);
246
+ if (![".ts", ".js"].includes(ext)) return [];
247
+ const baseName = path4.basename(entry.name, ext);
245
248
  const apiSegments = baseName === "route" || baseName === "index" ? parentSegments : [...parentSegments, baseName];
246
249
  const apiPath = "/api/" + apiSegments.join("/");
247
- routes.api.push({
250
+ return [{
248
251
  type: RouteType.API,
249
252
  path: apiPath.replace(/\/+/g, "/") || "/api",
250
253
  filePath: fullPath,
251
254
  pattern: createRoutePattern(apiPath),
252
255
  segments: ["api", ...apiSegments].filter(Boolean)
253
- });
256
+ }];
254
257
  }
255
- }
256
- }
257
- function createRoutePattern(routePath) {
258
- let pattern = routePath.replace(/\*[^/]*/g, "(.*)").replace(/:[^/]+/g, "([^/]+)").replace(/\//g, "\\/");
259
- return new RegExp(`^${pattern}$`);
260
- }
261
- function matchRoute(urlPath, routes) {
262
- const normalizedPath = urlPath === "" ? "/" : urlPath.split("?")[0];
263
- for (const route of routes) {
264
- const match = normalizedPath.match(route.pattern);
265
- if (match) {
266
- const params = extractParams(route.path, match);
267
- return { ...route, params };
268
- }
269
- }
270
- return null;
271
- }
272
- function extractParams(routePath, match) {
273
- const params = {};
274
- const paramNames = [];
275
- const paramRegex = /:([^/]+)|\*([^/]*)/g;
276
- let paramMatch;
277
- while ((paramMatch = paramRegex.exec(routePath)) !== null) {
278
- paramNames.push(paramMatch[1] || paramMatch[2] || "splat");
279
- }
280
- paramNames.forEach((name, index) => {
281
- params[name] = match[index + 1];
258
+ return [];
282
259
  });
283
- return params;
260
+ const subRoutes = await Promise.all(promises);
261
+ return routes.concat(...subRoutes);
284
262
  }
263
+
264
+ // src/router/tree-builder.ts
285
265
  function findRouteLayouts(route, layoutsMap) {
286
266
  const layouts = [];
287
267
  for (const segment of route.segments) {
@@ -311,6 +291,44 @@ function buildTree(routes) {
311
291
  }
312
292
  return tree;
313
293
  }
294
+
295
+ // src/router/matcher.ts
296
+ function matchRoute(urlPath, routes) {
297
+ const normalizedPath = urlPath === "" ? "/" : urlPath.split("?")[0];
298
+ for (const route of routes) {
299
+ const match = normalizedPath.match(route.pattern);
300
+ if (match) {
301
+ const params = extractParams(route.path, match);
302
+ return { ...route, params };
303
+ }
304
+ }
305
+ return null;
306
+ }
307
+
308
+ // src/router/index.ts
309
+ async function buildRouteTree(appDir) {
310
+ const projectRoot = path4.dirname(appDir);
311
+ const routes = {
312
+ pages: [],
313
+ api: [],
314
+ layouts: /* @__PURE__ */ new Map(),
315
+ tree: { children: {}, routes: [] },
316
+ appRoutes: []
317
+ };
318
+ if (fs.existsSync(appDir)) {
319
+ routes.appRoutes = await scanAppDirectory(appDir, appDir);
320
+ }
321
+ const serverApiDir = path4.join(projectRoot, "server", "api");
322
+ if (fs.existsSync(serverApiDir)) {
323
+ routes.api = await scanApiDirectory(serverApiDir, serverApiDir);
324
+ }
325
+ const rootLayoutTsx = path4.join(appDir, "layout.tsx");
326
+ const rootLayoutJsx = path4.join(appDir, "layout.jsx");
327
+ if (fs.existsSync(rootLayoutTsx)) routes.rootLayout = rootLayoutTsx;
328
+ else if (fs.existsSync(rootLayoutJsx)) routes.rootLayout = rootLayoutJsx;
329
+ routes.tree = buildTree(routes.appRoutes);
330
+ return routes;
331
+ }
314
332
  var AppConfigSchema = z.object({
315
333
  name: z.string().default("Velix App"),
316
334
  url: z.string().url().optional()
@@ -374,10 +392,10 @@ function defineConfig(config) {
374
392
  return config;
375
393
  }
376
394
  async function loadConfig(projectRoot) {
377
- const configPathTs = path2.join(projectRoot, "velix.config.ts");
378
- const configPathJs = path2.join(projectRoot, "velix.config.js");
379
- const configPathLegacyTs = path2.join(projectRoot, "flexireact.config.ts");
380
- const configPathLegacyJs = path2.join(projectRoot, "flexireact.config.js");
395
+ const configPathTs = path4.join(projectRoot, "velix.config.ts");
396
+ const configPathJs = path4.join(projectRoot, "velix.config.js");
397
+ const configPathLegacyTs = path4.join(projectRoot, "flexireact.config.ts");
398
+ const configPathLegacyJs = path4.join(projectRoot, "flexireact.config.js");
381
399
  let configPath = null;
382
400
  if (fs.existsSync(configPathTs)) configPath = configPathTs;
383
401
  else if (fs.existsSync(configPathJs)) configPath = configPathJs;
@@ -410,9 +428,9 @@ async function loadConfig(projectRoot) {
410
428
  function resolvePaths(config, projectRoot) {
411
429
  return {
412
430
  ...config,
413
- resolvedAppDir: path2.resolve(projectRoot, config.appDir),
414
- resolvedPublicDir: path2.resolve(projectRoot, config.publicDir),
415
- resolvedOutDir: path2.resolve(projectRoot, config.build.outDir)
431
+ resolvedAppDir: path4.resolve(projectRoot, config.appDir),
432
+ resolvedPublicDir: path4.resolve(projectRoot, config.publicDir),
433
+ resolvedOutDir: path4.resolve(projectRoot, config.build.outDir)
416
434
  };
417
435
  }
418
436
  function deepMerge(target, source) {
@@ -428,7 +446,7 @@ function deepMerge(target, source) {
428
446
  }
429
447
 
430
448
  // src/version.ts
431
- var VERSION = "5.2.8";
449
+ var VERSION = "5.3.0";
432
450
 
433
451
  // src/logger.ts
434
452
  var colors = {
@@ -476,14 +494,14 @@ var logger = {
476
494
  if (pagesDir) console.log(` ${c.bold}App:${c.reset} ${c.dim}${pagesDir}${c.reset}`);
477
495
  console.log("");
478
496
  },
479
- request(method, path8, status, time, extra = {}) {
497
+ request(method, path9, status, time, extra = {}) {
480
498
  const statusColor = getStatusColor(status);
481
499
  const timeStr = fmtTime(time);
482
500
  let badge = `${c.dim}\u25CB${c.reset}`;
483
501
  if (extra.type === "dynamic" || extra.type === "ssr") badge = `${c.white}\u0192${c.reset}`;
484
502
  else if (extra.type === "api") badge = `${c.cyan}\u03BB${c.reset}`;
485
503
  const statusStr = `${statusColor}${status}${c.reset}`;
486
- console.log(` ${badge} ${c.white}${method}${c.reset} ${path8} ${statusStr} ${c.dim}${timeStr}${c.reset}`);
504
+ console.log(` ${badge} ${c.white}${method}${c.reset} ${path9} ${statusStr} ${c.dim}${timeStr}${c.reset}`);
487
505
  },
488
506
  info(msg) {
489
507
  console.log(` ${c.cyan}\u2139${c.reset} ${msg}`);
@@ -511,10 +529,10 @@ var logger = {
511
529
  plugin(name) {
512
530
  console.log(` ${c.cyan}\u25C6${c.reset} Plugin ${c.dim}${name}${c.reset}`);
513
531
  },
514
- route(path8, type) {
532
+ route(path9, type) {
515
533
  const typeLabel = type === "api" ? "\u03BB" : type === "dynamic" ? "\u0192" : "\u25CB";
516
534
  const color = type === "api" ? c.cyan : type === "dynamic" ? c.white : c.dim;
517
- console.log(` ${color}${typeLabel}${c.reset} ${path8}`);
535
+ console.log(` ${color}${typeLabel}${c.reset} ${path9}`);
518
536
  },
519
537
  divider() {
520
538
  console.log(`${c.dim} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}`);
@@ -805,7 +823,7 @@ async function loadMiddleware(projectRoot) {
805
823
  const fns = [];
806
824
  const possibleFiles = ["proxy.ts", "proxy.js"];
807
825
  for (const file of possibleFiles) {
808
- const filePath = path2.join(projectRoot, file);
826
+ const filePath = path4.join(projectRoot, file);
809
827
  if (!fs.existsSync(filePath)) continue;
810
828
  try {
811
829
  const { pathToFileURL: pathToFileURL4 } = await import('url');
@@ -980,9 +998,9 @@ async function loadPlugins(projectRoot, config) {
980
998
  for (const entry of pluginEntries) {
981
999
  try {
982
1000
  if (typeof entry === "string") {
983
- const localPath = path2.join(projectRoot, "plugins", entry + ".ts");
984
- const localPathJs = path2.join(projectRoot, "plugins", entry + ".js");
985
- const localPathDir = path2.join(projectRoot, "plugins", entry, "index.ts");
1001
+ const localPath = path4.join(projectRoot, "plugins", entry + ".ts");
1002
+ const localPathJs = path4.join(projectRoot, "plugins", entry + ".js");
1003
+ const localPathDir = path4.join(projectRoot, "plugins", entry, "index.ts");
986
1004
  let pluginPath = null;
987
1005
  if (fs.existsSync(localPath)) pluginPath = localPath;
988
1006
  else if (fs.existsSync(localPathJs)) pluginPath = localPathJs;
@@ -1523,189 +1541,113 @@ function createAIAction(config) {
1523
1541
  return response.text;
1524
1542
  };
1525
1543
  }
1526
-
1527
- // src/cache/index.ts
1528
- var VelixCache = class {
1529
- maxSize;
1530
- defaultTtl;
1531
- defaultSwr;
1532
- store = /* @__PURE__ */ new Map();
1544
+ var MemoryCacheAdapter = class {
1545
+ lru;
1533
1546
  tagIndex = /* @__PURE__ */ new Map();
1534
- // LRU doubly-linked list sentinel nodes
1535
- head;
1536
- tail;
1537
- constructor(config = {}) {
1538
- this.maxSize = config.maxSize ?? 1e3;
1539
- this.defaultTtl = config.defaultTtl ?? 6e4;
1540
- this.defaultSwr = config.defaultSwr ?? 1e4;
1541
- this.head = this.createSentinel("__HEAD__");
1542
- this.tail = this.createSentinel("__TAIL__");
1543
- this.head.next = this.tail;
1544
- this.tail.prev = this.head;
1545
- }
1546
- /** Current number of entries */
1547
- get size() {
1548
- return this.store.size;
1547
+ constructor(options = {}) {
1548
+ this.lru = new LRUCache({ max: options.maxSize ?? 500 });
1549
1549
  }
1550
- /**
1551
- * Store a value with optional TTL, SWR, and tags.
1552
- */
1553
- set(key, value, options) {
1554
- if (this.store.has(key)) {
1555
- this.removeEntry(this.store.get(key));
1550
+ async get(key) {
1551
+ const entry = this.lru.get(key);
1552
+ if (!entry) return null;
1553
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
1554
+ this.lru.delete(key);
1555
+ return null;
1556
1556
  }
1557
+ return entry.value;
1558
+ }
1559
+ async set(key, value, options = {}) {
1557
1560
  const entry = {
1558
- key,
1559
1561
  value,
1560
- tags: new Set(options?.tags ?? []),
1561
- createdAt: Date.now(),
1562
- ttl: options?.ttl ?? this.defaultTtl,
1563
- swr: options?.swr ?? this.defaultSwr,
1564
- prev: null,
1565
- next: null
1562
+ tags: options.tags ?? [],
1563
+ expiresAt: options.ttl ? Date.now() + options.ttl : null
1566
1564
  };
1567
- this.store.set(key, entry);
1568
- this.addToHead(entry);
1569
- for (const tag of entry.tags) {
1570
- if (!this.tagIndex.has(tag)) {
1571
- this.tagIndex.set(tag, /* @__PURE__ */ new Set());
1572
- }
1565
+ this.lru.set(key, entry);
1566
+ options.tags?.forEach((tag) => {
1567
+ if (!this.tagIndex.has(tag)) this.tagIndex.set(tag, /* @__PURE__ */ new Set());
1573
1568
  this.tagIndex.get(tag).add(key);
1574
- }
1575
- while (this.store.size > this.maxSize) {
1576
- this.evictLRU();
1577
- }
1569
+ });
1578
1570
  }
1579
- /**
1580
- * Get a cached value. Returns `null` if not found or expired beyond SWR window.
1581
- *
1582
- * @returns `{ value, stale }` — `stale` is true when the entry is past TTL but within SWR window.
1583
- */
1584
- get(key) {
1585
- const entry = this.store.get(key);
1586
- if (!entry) return null;
1587
- const now = Date.now();
1588
- const age = now - entry.createdAt;
1589
- if (age > entry.ttl + entry.swr) {
1590
- this.removeEntry(entry);
1591
- return null;
1592
- }
1593
- this.moveToHead(entry);
1594
- const stale = age > entry.ttl;
1595
- return { value: entry.value, stale };
1571
+ async delete(key) {
1572
+ this.lru.delete(key);
1596
1573
  }
1597
- /**
1598
- * Simple get that returns only the value (ignoring staleness).
1599
- * Returns `null` if not found or expired.
1600
- */
1601
- getValue(key) {
1602
- const result = this.get(key);
1603
- return result ? result.value : null;
1604
- }
1605
- /** Check whether a key exists and is not expired */
1606
- has(key) {
1607
- return this.get(key) !== null;
1608
- }
1609
- /** Remove a specific key */
1610
- delete(key) {
1611
- const entry = this.store.get(key);
1612
- if (!entry) return false;
1613
- this.removeEntry(entry);
1614
- return true;
1615
- }
1616
- /** Invalidate a specific path/key */
1617
- revalidatePath(path8) {
1618
- this.delete(path8);
1619
- }
1620
- /** Invalidate all entries associated with a tag */
1621
- revalidateTag(tag) {
1574
+ async deleteByTag(tag) {
1622
1575
  const keys = this.tagIndex.get(tag);
1623
1576
  if (!keys) return;
1624
- for (const key of keys) {
1625
- const entry = this.store.get(key);
1626
- if (entry) {
1627
- this.removeEntry(entry);
1628
- }
1629
- }
1577
+ keys.forEach((key) => this.lru.delete(key));
1630
1578
  this.tagIndex.delete(tag);
1631
1579
  }
1632
- /** Clear all entries */
1633
- invalidateAll() {
1634
- this.store.clear();
1635
- this.tagIndex.clear();
1636
- this.head.next = this.tail;
1637
- this.tail.prev = this.head;
1580
+ async deleteByPrefix(prefix) {
1581
+ for (const key of this.lru.keys()) {
1582
+ if (key.startsWith(prefix)) this.lru.delete(key);
1583
+ }
1638
1584
  }
1639
- /** Get cache stats for debugging/monitoring */
1640
- stats() {
1641
- return {
1642
- size: this.store.size,
1643
- maxSize: this.maxSize,
1644
- tags: this.tagIndex.size
1645
- };
1585
+ async clear() {
1586
+ this.lru.clear();
1587
+ this.tagIndex.clear();
1646
1588
  }
1647
- // ── Internal LRU operations ──
1648
- createSentinel(key) {
1649
- return {
1650
- key,
1651
- value: null,
1652
- tags: /* @__PURE__ */ new Set(),
1653
- createdAt: 0,
1654
- ttl: 0,
1655
- swr: 0,
1656
- prev: null,
1657
- next: null
1658
- };
1589
+ async has(key) {
1590
+ return this.lru.has(key);
1659
1591
  }
1660
- addToHead(entry) {
1661
- entry.prev = this.head;
1662
- entry.next = this.head.next;
1663
- this.head.next.prev = entry;
1664
- this.head.next = entry;
1665
- }
1666
- removeFromList(entry) {
1667
- if (entry.prev) entry.prev.next = entry.next;
1668
- if (entry.next) entry.next.prev = entry.prev;
1669
- entry.prev = null;
1670
- entry.next = null;
1671
- }
1672
- moveToHead(entry) {
1673
- this.removeFromList(entry);
1674
- this.addToHead(entry);
1675
- }
1676
- evictLRU() {
1677
- const lru = this.tail.prev;
1678
- if (lru && lru !== this.head) {
1679
- this.removeEntry(lru);
1592
+ };
1593
+
1594
+ // src/cache/deduplicator.ts
1595
+ var RequestDeduplicator = class {
1596
+ inFlight = /* @__PURE__ */ new Map();
1597
+ async dedupe(key, fn) {
1598
+ if (this.inFlight.has(key)) {
1599
+ return this.inFlight.get(key);
1680
1600
  }
1601
+ const promise = fn().finally(() => {
1602
+ this.inFlight.delete(key);
1603
+ });
1604
+ this.inFlight.set(key, promise);
1605
+ return promise;
1681
1606
  }
1682
- removeEntry(entry) {
1683
- this.removeFromList(entry);
1684
- this.store.delete(entry.key);
1685
- for (const tag of entry.tags) {
1686
- const keys = this.tagIndex.get(tag);
1687
- if (keys) {
1688
- keys.delete(entry.key);
1689
- if (keys.size === 0) {
1690
- this.tagIndex.delete(tag);
1691
- }
1692
- }
1693
- }
1607
+ };
1608
+
1609
+ // src/cache/index.ts
1610
+ var VelixCache = class {
1611
+ adapter;
1612
+ deduplicator;
1613
+ constructor(adapter = new MemoryCacheAdapter()) {
1614
+ this.adapter = adapter;
1615
+ this.deduplicator = new RequestDeduplicator();
1616
+ }
1617
+ async revalidatePath(path9) {
1618
+ await this.adapter.deleteByPrefix(`route:${path9}`);
1619
+ }
1620
+ async revalidateTag(tag) {
1621
+ await this.adapter.deleteByTag(tag);
1622
+ }
1623
+ // unstable_cache — avec déduplication
1624
+ unstable_cache(fn, keys, options) {
1625
+ const key = keys.join(":");
1626
+ return async () => {
1627
+ const cached = await this.adapter.get(key);
1628
+ if (cached !== null) return cached;
1629
+ const value = await this.deduplicator.dedupe(key, fn);
1630
+ await this.adapter.set(key, value, {
1631
+ ttl: options?.revalidate ? options.revalidate * 1e3 : void 0,
1632
+ tags: options?.tags
1633
+ });
1634
+ return value;
1635
+ };
1694
1636
  }
1695
1637
  };
1696
1638
  var cacheManager = new VelixCache();
1697
- function revalidatePath(path8, _type = "path") {
1698
- cacheManager.revalidatePath(path8);
1639
+ async function revalidatePath(path9, _type = "path") {
1640
+ await cacheManager.revalidatePath(path9);
1699
1641
  if (typeof global !== "undefined" && global.__VELIX_HMR_SERVER__) {
1700
1642
  global.__VELIX_HMR_SERVER__.broadcast(JSON.stringify({
1701
1643
  type: "revalidate",
1702
- path: path8,
1644
+ path: path9,
1703
1645
  revalidationType: _type
1704
1646
  }));
1705
1647
  }
1706
1648
  }
1707
- function revalidateTag(tag) {
1708
- cacheManager.revalidateTag(tag);
1649
+ async function revalidateTag(tag) {
1650
+ await cacheManager.revalidateTag(tag);
1709
1651
  if (typeof global !== "undefined" && global.__VELIX_HMR_SERVER__) {
1710
1652
  global.__VELIX_HMR_SERVER__.broadcast(JSON.stringify({
1711
1653
  type: "revalidate",
@@ -1714,19 +1656,7 @@ function revalidateTag(tag) {
1714
1656
  }
1715
1657
  }
1716
1658
  function unstable_cache(fn, keys, options) {
1717
- return async () => {
1718
- const cacheKey = keys.join(":");
1719
- const cached = cacheManager.get(cacheKey);
1720
- if (cached && !cached.stale) {
1721
- return cached.value;
1722
- }
1723
- const result = await fn();
1724
- cacheManager.set(cacheKey, result, {
1725
- tags: options?.tags,
1726
- ttl: options?.revalidate ? options.revalidate * 1e3 : void 0
1727
- });
1728
- return result;
1729
- };
1659
+ return cacheManager.unstable_cache(fn, keys, options);
1730
1660
  }
1731
1661
  var VelixHttpError = class extends Error {
1732
1662
  status;
@@ -1783,7 +1713,7 @@ async function buildApiManifest(serverDir) {
1783
1713
  return files.map((file) => filePathToRoute(file, serverDir));
1784
1714
  }
1785
1715
  function filePathToRoute(filePath, serverDir) {
1786
- const relative = path2.relative(path2.join(serverDir, "api"), filePath);
1716
+ const relative = path4.relative(path4.join(serverDir, "api"), filePath);
1787
1717
  const normalizedRelative = relative.replace(/\\/g, "/");
1788
1718
  const withoutExt = normalizedRelative.replace(/\.ts$/, "");
1789
1719
  const params = [];
@@ -1873,7 +1803,7 @@ function serverAction(options) {
1873
1803
  }
1874
1804
  };
1875
1805
  }
1876
- function createHMRServer(httpServer, projectRoot) {
1806
+ function createHMRServer(httpServer, projectRoot, options) {
1877
1807
  const wss = new WebSocketServer({ server: httpServer, path: "/__velix_hmr" });
1878
1808
  const clients = /* @__PURE__ */ new Set();
1879
1809
  wss.on("connection", (ws) => {
@@ -1886,19 +1816,26 @@ function createHMRServer(httpServer, projectRoot) {
1886
1816
  persistent: true,
1887
1817
  ignoreInitial: true
1888
1818
  });
1889
- function broadcast(event) {
1819
+ function broadcastLocal(event) {
1890
1820
  const msg = JSON.stringify(event);
1891
1821
  clients.forEach((ws) => {
1892
1822
  if (ws.readyState === ws.OPEN) ws.send(msg);
1893
1823
  });
1894
1824
  }
1825
+ function broadcast(event) {
1826
+ broadcastLocal(event);
1827
+ options?.pubsub?.publish(event);
1828
+ }
1829
+ options?.pubsub?.onEvent((event) => {
1830
+ broadcastLocal(event);
1831
+ });
1895
1832
  watcher.on("change", (file) => {
1896
1833
  broadcast({ type: "file-changed", file, timestamp: Date.now() });
1897
1834
  });
1898
1835
  watcher.on("add", (file) => {
1899
1836
  broadcast({ type: "file-added", file, timestamp: Date.now() });
1900
1837
  });
1901
- return { broadcast, watcher, wss, clients };
1838
+ return { broadcast, broadcastLocal, watcher, wss, clients };
1902
1839
  }
1903
1840
  function getErrorBoundaryType(error) {
1904
1841
  if (error instanceof NotFoundError) return "not-found";
@@ -1906,18 +1843,18 @@ function getErrorBoundaryType(error) {
1906
1843
  return "error";
1907
1844
  }
1908
1845
  function resolveErrorBoundary(routeFilePath, appDir, type) {
1909
- let currentDir = path2.dirname(routeFilePath);
1846
+ let currentDir = path4.dirname(routeFilePath);
1910
1847
  while (currentDir.length >= appDir.length && currentDir.startsWith(appDir)) {
1911
- const boundaryPath = path2.join(currentDir, `${type}.tsx`);
1848
+ const boundaryPath = path4.join(currentDir, `${type}.tsx`);
1912
1849
  if (fs.existsSync(boundaryPath)) {
1913
- const scope = currentDir === appDir ? "root" : path2.basename(currentDir);
1850
+ const scope = currentDir === appDir ? "root" : path4.basename(currentDir);
1914
1851
  return {
1915
1852
  filePath: boundaryPath,
1916
1853
  type,
1917
1854
  scope
1918
1855
  };
1919
1856
  }
1920
- const parentDir = path2.dirname(currentDir);
1857
+ const parentDir = path4.dirname(currentDir);
1921
1858
  if (parentDir === currentDir) break;
1922
1859
  currentDir = parentDir;
1923
1860
  }
@@ -1932,6 +1869,6 @@ function defineNotFound(component) {
1932
1869
  return component;
1933
1870
  }
1934
1871
 
1935
- export { ForbiddenError, NotFoundError, OllamaProvider, OpenAIProvider, PluginHooks, PluginManager, RouteType, UnauthorizedError, VelixCache, VelixConfigSchema, VelixHttpError, buildApiManifest, buildPrompt, buildRouteTree, builtinPlugins, cacheManager, cleanDir, composeMiddleware, copyDir, createAIAction, createAIClient, createDeferred, createHMRServer, createStreamDecoder, debounce, defaultConfig, defineConfig, defineError, defineLoader, defineNotFound, definePlugin, defineRoute, ensureDir, escapeHtml, estimateTokens, filePathToRoute, findFiles, findRouteLayouts, formatBytes, formatTime, generateHash, generateJsonLd, generateMetadataTags, generateRobotsTxt, generateSitemap, getErrorBoundaryType, handleApiRequest, isClientComponent, isIsland, isServerComponent, jsonLd, loadConfig, loadMiddleware, loadPlugins, logger, matchRoute, mergeMetadata, middlewares, parseSSE, pluginManager, resolveErrorBoundary, resolvePaths, retry, revalidatePath, revalidateTag, runMiddleware, safeJsonParse, sanitizeInput, serverAction, sleep, truncateToTokens, unstable_cache, useAI, validateApiKey, validateInput };
1872
+ export { ForbiddenError, NotFoundError, OllamaProvider, OpenAIProvider, PluginHooks, PluginManager, RouteType, UnauthorizedError, VelixCache, VelixConfigSchema, VelixHttpError, buildApiManifest, buildPrompt, buildRouteTree, builtinPlugins, cacheManager, cleanDir, composeMiddleware, copyDir, createAIAction, createAIClient, createDeferred, createHMRServer, createRoutePattern, createStreamDecoder, debounce, defaultConfig, defineConfig, defineError, defineLoader, defineNotFound, definePlugin, defineRoute, ensureDir, escapeHtml, estimateTokens, filePathToRoute, findFiles, findRouteLayouts, formatBytes, formatTime, generateHash, generateJsonLd, generateMetadataTags, generateRobotsTxt, generateSitemap, getErrorBoundaryType, handleApiRequest, isClientComponent, isIsland, isServerComponent, jsonLd, loadConfig, loadMiddleware, loadPlugins, logger, matchRoute, mergeMetadata, middlewares, parseSSE, pluginManager, resolveErrorBoundary, resolvePaths, retry, revalidatePath, revalidateTag, runMiddleware, safeJsonParse, sanitizeInput, serverAction, sleep, truncateToTokens, unstable_cache, useAI, validateApiKey, validateInput };
1936
1873
  //# sourceMappingURL=index.js.map
1937
1874
  //# sourceMappingURL=index.js.map