fynixui 1.0.11 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fynixui",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "type": "module",
5
5
  "description": "Core package for Fynix UI framework - A lightweight, reactive UI framework with TypeScript support.",
6
6
  "main": "./dist/fynix/index.js",
@@ -214,6 +214,5 @@
214
214
  "./types/global.d.ts"
215
215
  ]
216
216
  }
217
- },
218
- "dependencies": {}
217
+ }
219
218
  }
@@ -1,8 +1,18 @@
1
1
  import { transform } from "esbuild";
2
2
  import { normalizePath } from "vite";
3
+ import crypto from "crypto";
3
4
  export default function fynixPlugin(options = {}) {
4
- const { jsxFactory = "Fynix", jsxFragment = "Fynix.Fragment", include = [".ts", ".js", ".jsx", ".tsx", ".fnx"], exclude = ["node_modules"], sourcemap = true, esbuildOptions = {}, } = options;
5
+ const { jsxFactory = "Fynix", jsxFragment = "Fynix.Fragment", include = [".ts", ".js", ".jsx", ".tsx", ".fnx"], exclude = ["node_modules"], esbuildOptions = {}, } = options;
5
6
  let viteServer = null;
7
+ const transformCache = new Map();
8
+ function hasJsxLike(code) {
9
+ return (/<[A-Za-z]/.test(code) ||
10
+ /Fynix\s*\(/.test(code) ||
11
+ /Fragment|<>/.test(code));
12
+ }
13
+ function getContentHash(code) {
14
+ return crypto.createHash("md5").update(code).digest("hex");
15
+ }
6
16
  return {
7
17
  name: "vite-plugin-fynix",
8
18
  enforce: "pre",
@@ -17,6 +27,14 @@ export default function fynixPlugin(options = {}) {
17
27
  const shouldInclude = include.some((ext) => normalizedId.endsWith(ext));
18
28
  if (!shouldInclude)
19
29
  return null;
30
+ if (!hasJsxLike(code)) {
31
+ return null;
32
+ }
33
+ const contentHash = getContentHash(code);
34
+ const cached = transformCache.get(normalizedId);
35
+ if (cached && cached.contentHash === contentHash) {
36
+ return { code: cached.code, map: cached.map };
37
+ }
20
38
  const ctx = this;
21
39
  if (typeof ctx.addWatchFile === "function") {
22
40
  ctx.addWatchFile(id);
@@ -39,16 +57,19 @@ export default function fynixPlugin(options = {}) {
39
57
  loader,
40
58
  jsxFactory,
41
59
  jsxFragment,
42
- sourcemap,
60
+ sourcemap: false,
43
61
  sourcefile: id,
44
62
  target: "esnext",
45
63
  format: "esm",
46
64
  ...esbuildOptions,
47
65
  });
48
- return {
66
+ const transformed = {
49
67
  code: result.code,
50
- map: result.map || null,
68
+ map: null,
69
+ contentHash,
51
70
  };
71
+ transformCache.set(normalizedId, transformed);
72
+ return { code: transformed.code, map: transformed.map };
52
73
  }
53
74
  catch (error) {
54
75
  const err = error;
@@ -71,6 +92,7 @@ export default function fynixPlugin(options = {}) {
71
92
  handleHotUpdate(ctx) {
72
93
  const { file, server } = ctx;
73
94
  const normalizedFile = normalizePath(file);
95
+ transformCache.delete(normalizedFile);
74
96
  const shouldReload = include.some((ext) => normalizedFile.endsWith(ext));
75
97
  if (shouldReload) {
76
98
  console.log(`\x1b[32m[vite-plugin-fynix]\x1b[0m HMR: full-reload triggered by ${normalizedFile}`);
@@ -1,4 +1,36 @@
1
1
  import { mount } from "../runtime";
2
+ import { nixLazy } from "../hooks/nixLazy";
3
+ class LocationManager {
4
+ constructor() {
5
+ this.current = {
6
+ path: typeof window !== "undefined" ? window.location.pathname : "/",
7
+ params: {},
8
+ search: typeof window !== "undefined" ? window.location.search : "",
9
+ };
10
+ this.subscribers = new Set();
11
+ }
12
+ get value() {
13
+ return this.current;
14
+ }
15
+ set value(newLocation) {
16
+ this.current = newLocation;
17
+ this.subscribers.forEach((callback) => {
18
+ try {
19
+ callback(newLocation);
20
+ }
21
+ catch (error) {
22
+ console.error("[Router] Location subscriber error:", error);
23
+ }
24
+ });
25
+ }
26
+ subscribe(callback) {
27
+ this.subscribers.add(callback);
28
+ return () => {
29
+ this.subscribers.delete(callback);
30
+ };
31
+ }
32
+ }
33
+ export const location = new LocationManager();
2
34
  const MAX_CACHE_SIZE = 50;
3
35
  const PROPS_NAMESPACE = "__fynixLinkProps__";
4
36
  const MAX_LISTENERS = 100;
@@ -42,6 +74,30 @@ function sanitizeContent(content) {
42
74
  .replace(/expression\s*\(/gi, "");
43
75
  }
44
76
  function sanitizeProps(props) {
77
+ const sanitized = {};
78
+ for (const [key, value] of Object.entries(props)) {
79
+ if (typeof key !== "string" || key.startsWith("__")) {
80
+ continue;
81
+ }
82
+ if (typeof value === "string") {
83
+ const cleanContent = sanitizeContent(value);
84
+ sanitized[key] = escapeHTML(cleanContent);
85
+ }
86
+ else if (typeof value === "object" && value !== null) {
87
+ if (Object.keys(value).length < 50) {
88
+ sanitized[key] = sanitizeProps(value);
89
+ }
90
+ }
91
+ else if (typeof value === "function") {
92
+ sanitized[key] = value;
93
+ }
94
+ else if (typeof value === "number" || typeof value === "boolean") {
95
+ sanitized[key] = value;
96
+ }
97
+ }
98
+ return sanitized;
99
+ }
100
+ function sanitizeExternalProps(props) {
45
101
  const sanitized = {};
46
102
  for (const [key, value] of Object.entries(props)) {
47
103
  if (typeof key !== "string" ||
@@ -51,12 +107,11 @@ function sanitizeProps(props) {
51
107
  continue;
52
108
  }
53
109
  if (typeof value === "string") {
54
- const cleanContent = sanitizeContent(value);
55
- sanitized[key] = escapeHTML(cleanContent);
110
+ sanitized[key] = escapeHTML(sanitizeContent(value));
56
111
  }
57
112
  else if (typeof value === "object" && value !== null) {
58
113
  if (Object.keys(value).length < 50) {
59
- sanitized[key] = sanitizeProps(value);
114
+ sanitized[key] = sanitizeExternalProps(value);
60
115
  }
61
116
  }
62
117
  else if (typeof value === "number" || typeof value === "boolean") {
@@ -134,18 +189,37 @@ function sanitizePath(path) {
134
189
  }
135
190
  return path || "/";
136
191
  }
137
- function tryGlobPaths() {
192
+ function tryGlobPaths(lazy = false) {
138
193
  try {
139
- let modules = import.meta.glob("/src/**/*.{fnx,tsx,jsx,ts,js}", {
140
- eager: true,
141
- });
142
- if (Object.keys(modules).length === 0) {
143
- modules = import.meta.glob(["./**/*.fnx", "./**/*.tsx", "./**/*.jsx", "./**/*.ts", "./**/*.js"], { eager: true });
194
+ if (lazy) {
195
+ let modules = import.meta.glob("/src/**/*.{tsx,jsx,ts,js}");
196
+ if (Object.keys(modules).length === 0) {
197
+ modules = import.meta.glob([
198
+ "./**/*.tsx",
199
+ "./**/*.jsx",
200
+ "./**/*.ts",
201
+ "./**/*.js",
202
+ ]);
203
+ }
204
+ if (Object.keys(modules).length === 0) {
205
+ modules = import.meta.glob(["../**/*.tsx", "../**/*.jsx"]);
206
+ }
207
+ return modules || {};
144
208
  }
145
- if (Object.keys(modules).length === 0) {
146
- modules = import.meta.glob(["../**/*.fnx", "../**/*.tsx", "../**/*.jsx"], { eager: true });
209
+ else {
210
+ let modules = import.meta.glob("/src/**/*.{tsx,jsx,ts,js}", {
211
+ eager: true,
212
+ });
213
+ if (Object.keys(modules).length === 0) {
214
+ modules = import.meta.glob(["./**/*.tsx", "./**/*.jsx", "./**/*.ts", "./**/*.js"], { eager: true });
215
+ }
216
+ if (Object.keys(modules).length === 0) {
217
+ modules = import.meta.glob(["../**/*.tsx", "../**/*.jsx"], {
218
+ eager: true,
219
+ });
220
+ }
221
+ return modules || {};
147
222
  }
148
- return modules || {};
149
223
  }
150
224
  catch (error) {
151
225
  console.error("[Router] Failed to load modules:", error);
@@ -155,7 +229,7 @@ function tryGlobPaths() {
155
229
  function filePathToRoute(filePath) {
156
230
  let route = filePath
157
231
  .replace(/^.*\/src/, "")
158
- .replace(/\.(ts|tsx|js|jsx|fnx)$/, "")
232
+ .replace(/\.(ts|tsx|js|jsx)$/, "")
159
233
  .replace(/\/view$/, "")
160
234
  .replace(/\/$/, "");
161
235
  if (!route)
@@ -266,175 +340,7 @@ function updateMetaTags(meta = {}) {
266
340
  el.setAttribute("content", sanitizedValue);
267
341
  });
268
342
  }
269
- class EnterpriseRouter {
270
- constructor() {
271
- this.routeCache = new Map();
272
- this.preloadQueue = new Set();
273
- this.routeMatchCache = new Map();
274
- this.routes = {};
275
- }
276
- setRoutes(routes) {
277
- if (!routes || typeof routes !== "object") {
278
- console.warn("[EnterpriseRouter] Invalid routes configuration");
279
- return;
280
- }
281
- this.routes = routes;
282
- }
283
- async preloadRoute(path) {
284
- if (this.routeCache.has(path))
285
- return;
286
- const route = this.routes[path];
287
- if (route?.component) {
288
- const loadRoute = async () => {
289
- try {
290
- const component = await route.component();
291
- this.routeCache.set(path, component);
292
- route.prefetch?.forEach((prefetchPath) => {
293
- this.preloadQueue.add(prefetchPath);
294
- });
295
- console.log(`[EnterpriseRouter] Preloaded route: ${path}`);
296
- }
297
- catch (error) {
298
- console.warn(`[EnterpriseRouter] Failed to preload route ${path}:`, error);
299
- }
300
- };
301
- if ("requestIdleCallback" in window) {
302
- requestIdleCallback(loadRoute);
303
- }
304
- else {
305
- setTimeout(loadRoute, 0);
306
- }
307
- }
308
- }
309
- matchRoute(path) {
310
- const cached = this.routeMatchCache.get(path);
311
- if (cached !== undefined)
312
- return cached;
313
- const match = this.computeRouteMatch(path);
314
- if (this.routeMatchCache.size > 100) {
315
- const firstKey = this.routeMatchCache.keys().next().value;
316
- if (firstKey !== undefined) {
317
- this.routeMatchCache.delete(firstKey);
318
- }
319
- }
320
- this.routeMatchCache.set(path, match);
321
- return match;
322
- }
323
- computeRouteMatch(path) {
324
- const segments = path.split("/").filter(Boolean);
325
- for (const [routePath, routeConfig] of Object.entries(this.routes)) {
326
- const routeSegments = routePath.split("/").filter(Boolean);
327
- if (segments.length !== routeSegments.length)
328
- continue;
329
- const params = {};
330
- let isMatch = true;
331
- for (let i = 0; i < segments.length; i++) {
332
- const segment = segments[i];
333
- const routeSegment = routeSegments[i];
334
- if (routeSegment && segment) {
335
- if (routeSegment.startsWith(":")) {
336
- params[routeSegment.slice(1)] = segment;
337
- }
338
- else if (segment !== routeSegment) {
339
- isMatch = false;
340
- break;
341
- }
342
- }
343
- else {
344
- isMatch = false;
345
- break;
346
- }
347
- }
348
- if (isMatch) {
349
- return {
350
- component: routeConfig.component,
351
- params,
352
- meta: typeof routeConfig.meta === "function"
353
- ? routeConfig.meta(params)
354
- : routeConfig.meta,
355
- };
356
- }
357
- }
358
- return null;
359
- }
360
- async checkRouteGuard(route, path, params) {
361
- if (!route.guard)
362
- return true;
363
- if (route.guard.canActivate) {
364
- const canActivate = await route.guard.canActivate(path, params);
365
- return canActivate;
366
- }
367
- return true;
368
- }
369
- getPreloadedComponent(path) {
370
- return this.routeCache.get(path);
371
- }
372
- clearCache() {
373
- this.routeCache.clear();
374
- this.routeMatchCache.clear();
375
- this.preloadQueue.clear();
376
- }
377
- }
378
- class LayoutRouter {
379
- constructor() {
380
- this.layoutCache = new Map();
381
- this.keepAliveComponents = new Map();
382
- }
383
- renderNestedRoutes(routes, segments) {
384
- if (segments.length === 0)
385
- return null;
386
- const [currentSegment, ...remainingSegments] = segments;
387
- const currentRoute = routes.find((r) => r.path === currentSegment);
388
- if (!currentRoute)
389
- return null;
390
- let content;
391
- if (remainingSegments.length > 0 && currentRoute.children) {
392
- content = this.renderNestedRoutes(currentRoute.children, remainingSegments);
393
- }
394
- else {
395
- if (currentRoute.keepAlive && currentSegment) {
396
- content = this.renderKeepAlive(currentRoute.component, currentSegment);
397
- }
398
- else {
399
- content = this.renderComponent(currentRoute.component);
400
- }
401
- }
402
- if (currentRoute.layout) {
403
- const layoutKey = `${currentSegment}_layout`;
404
- let layoutComponent = this.layoutCache.get(layoutKey);
405
- if (!layoutComponent) {
406
- layoutComponent = this.renderComponent(currentRoute.layout);
407
- this.layoutCache.set(layoutKey, layoutComponent);
408
- }
409
- return this.renderComponent(currentRoute.layout, { children: content });
410
- }
411
- return content;
412
- }
413
- renderKeepAlive(component, key) {
414
- if (this.keepAliveComponents.has(key)) {
415
- return this.keepAliveComponents.get(key);
416
- }
417
- const rendered = this.renderComponent(component);
418
- this.keepAliveComponents.set(key, rendered);
419
- return rendered;
420
- }
421
- renderComponent(component, props = {}) {
422
- try {
423
- return component(props);
424
- }
425
- catch (error) {
426
- console.error("[LayoutRouter] Component render error:", error);
427
- return null;
428
- }
429
- }
430
- cleanup() {
431
- this.layoutCache.clear();
432
- this.keepAliveComponents.clear();
433
- }
434
- }
435
- const enterpriseRouter = new EnterpriseRouter();
436
- const layoutRouter = new LayoutRouter();
437
- function createFynix() {
343
+ function createFynix(options = {}) {
438
344
  const isDevMode = import.meta.hot !== undefined;
439
345
  if (routerInstance && isRouterInitialized && !isDevMode) {
440
346
  console.warn("[Router] Router already initialized, returning existing instance");
@@ -462,13 +368,17 @@ function createFynix() {
462
368
  }
463
369
  const propsCache = window.__fynixPropsCache || new Map();
464
370
  window.__fynixPropsCache = propsCache;
465
- const modules = tryGlobPaths();
371
+ const isLazy = !!options.lazy;
372
+ const modules = tryGlobPaths(isLazy);
466
373
  const routes = {};
467
374
  const dynamicRoutes = [];
468
375
  for (const [filePath, mod] of Object.entries(modules)) {
469
376
  const routePath = filePathToRoute(filePath);
470
377
  let component = undefined;
471
- if (mod && typeof mod === "object") {
378
+ if (isLazy && typeof mod === "function") {
379
+ component = nixLazy(mod);
380
+ }
381
+ else if (mod && typeof mod === "object") {
472
382
  if ("default" in mod && mod.default) {
473
383
  component = mod.default;
474
384
  }
@@ -512,17 +422,6 @@ function createFynix() {
512
422
  let Page = routes[path];
513
423
  let params = {};
514
424
  let routeProps = {};
515
- const enterpriseMatch = enterpriseRouter.matchRoute(path);
516
- if (enterpriseMatch) {
517
- const preloadedComponent = enterpriseRouter.getPreloadedComponent(path);
518
- if (preloadedComponent) {
519
- Page = preloadedComponent;
520
- }
521
- else {
522
- Page = enterpriseMatch.component;
523
- }
524
- params = enterpriseMatch.params;
525
- }
526
425
  if (!Page) {
527
426
  const match = matchDynamicRoute(path, dynamicRoutes);
528
427
  if (match) {
@@ -557,9 +456,6 @@ function createFynix() {
557
456
  container.appendChild(backButton);
558
457
  root.appendChild(container);
559
458
  updateMetaTags({ title: "404 - Page Not Found" });
560
- ["/", "/home", "/about"].forEach((commonPath) => {
561
- enterpriseRouter.preloadRoute(commonPath).catch(console.warn);
562
- });
563
459
  return;
564
460
  }
565
461
  const state = (window.history.state || {});
@@ -577,13 +473,16 @@ function createFynix() {
577
473
  const meta = typeof Page.meta === "function" ? Page.meta(params) : Page.meta;
578
474
  updateMetaTags(meta);
579
475
  }
580
- const unsafeProps = {
581
- ...routeProps,
582
- ...passedProps,
583
- params,
476
+ const safeProps = {
477
+ ...sanitizeExternalProps(passedProps),
478
+ ...sanitizeProps({ ...routeProps, params }),
584
479
  };
585
- const safeProps = sanitizeProps(unsafeProps);
586
480
  window.__lastRouteProps = safeProps;
481
+ location.value = {
482
+ path,
483
+ params,
484
+ search: window.location.search,
485
+ };
587
486
  try {
588
487
  mount(Page, rootSelector, safeProps);
589
488
  }
@@ -624,7 +523,6 @@ function createFynix() {
624
523
  }
625
524
  if (normalizedPath === currentPath)
626
525
  return;
627
- enterpriseRouter.preloadRoute(normalizedPath).catch(console.warn);
628
526
  const sanitizedProps = sanitizeProps(props);
629
527
  const cacheKey = generateCacheKey();
630
528
  addToCache(propsCache, cacheKey, sanitizedProps);
@@ -729,11 +627,15 @@ function createFynix() {
729
627
  handler: clickHandler,
730
628
  });
731
629
  listenerCount++;
732
- window.addEventListener("popstate", renderRoute);
630
+ const popstateHandler = (_e) => {
631
+ if (!isDestroyed)
632
+ renderRoute();
633
+ };
634
+ window.addEventListener("popstate", popstateHandler);
733
635
  listeners.push({
734
636
  element: window,
735
637
  event: "popstate",
736
- handler: renderRoute,
638
+ handler: popstateHandler,
737
639
  });
738
640
  listenerCount++;
739
641
  }
@@ -756,8 +658,6 @@ function createFynix() {
756
658
  renderTimeout = null;
757
659
  }
758
660
  isDestroyed = true;
759
- enterpriseRouter.clearCache();
760
- layoutRouter.cleanup();
761
661
  listeners.forEach(({ element, event, handler }) => {
762
662
  try {
763
663
  element.removeEventListener(event, handler);
@@ -818,15 +718,6 @@ function createFynix() {
818
718
  cleanup,
819
719
  routes,
820
720
  dynamicRoutes,
821
- preloadRoute: enterpriseRouter.preloadRoute.bind(enterpriseRouter),
822
- clearCache: () => {
823
- enterpriseRouter.clearCache();
824
- layoutRouter.cleanup();
825
- },
826
- enableNestedRouting: (nestedRoutes) => {
827
- router.nestedRoutes = nestedRoutes;
828
- console.log("[Router] Nested routing enabled with", nestedRoutes.length, "routes");
829
- },
830
721
  };
831
722
  routerInstance = router;
832
723
  return router;