arcanajs 2.5.3 → 2.6.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.
@@ -9,6 +9,7 @@ exports.configFiles = [
9
9
  { src: "postcss.config.js", dest: "postcss.config.js" },
10
10
  // types
11
11
  { src: "src/arcanajs.d.ts", dest: "src/arcanajs.d.ts" },
12
+ { src: "src/types/HomePageData.ts", dest: "src/types/HomePageData.ts" },
12
13
  // client
13
14
  { src: "src/client/globals.css", dest: "src/client/globals.css" },
14
15
  { src: "src/client/index.tsx", dest: "src/client/index.tsx" },
@@ -54,6 +55,7 @@ exports.requiredDirs = [
54
55
  "src/client",
55
56
  "src/db",
56
57
  "src/server",
58
+ "src/types",
57
59
  "src/server/routes",
58
60
  "src/server/controllers",
59
61
  "src/views",
@@ -4,17 +4,20 @@ exports.Link = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const useRouter_1 = require("../hooks/useRouter");
6
6
  const Link = ({ href, children, prefetch = false, ...props }) => {
7
- const { navigateTo } = (0, useRouter_1.useRouter)();
7
+ const { navigateTo, navigateToAsync } = (0, useRouter_1.useRouter)();
8
8
  const isExternal = /^https?:\/\//.test(href);
9
- const handleClick = (e) => {
9
+ const handleClick = async (e) => {
10
10
  e.preventDefault();
11
- if (!isExternal) {
12
- navigateTo(href);
13
- }
14
- else {
11
+ if (isExternal) {
15
12
  // Open external links in a new tab
16
13
  window.open(href, "_blank", "noopener,noreferrer");
17
14
  }
15
+ else if (navigateToAsync) {
16
+ await navigateToAsync(href);
17
+ }
18
+ else {
19
+ navigateTo(href);
20
+ }
18
21
  };
19
22
  const handleMouseEnter = () => {
20
23
  if (prefetch && !isExternal) {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- export declare const Page: React.FC<{
3
- data?: any;
2
+ export declare const Page: <T>({ data, title, children, }: {
3
+ data?: T;
4
4
  title?: string;
5
5
  children: React.ReactNode;
6
- }>;
6
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -4,7 +4,7 @@ exports.Page = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const PageContext_1 = require("../context/PageContext");
6
6
  const Head_1 = require("./Head");
7
- const Page = ({ data, title, children }) => {
7
+ const Page = ({ data, title, children, }) => {
8
8
  return ((0, jsx_runtime_1.jsxs)(PageContext_1.PageContext.Provider, { value: data, children: [title && ((0, jsx_runtime_1.jsx)(Head_1.Head, { children: (0, jsx_runtime_1.jsx)("title", { children: title }) })), children] }));
9
9
  };
10
10
  exports.Page = Page;
@@ -2,4 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PageContext = void 0;
4
4
  const createSingletonContext_1 = require("../utils/createSingletonContext");
5
+ // PageContext holds the page data (may be null before hydration).
5
6
  exports.PageContext = (0, createSingletonContext_1.createSingletonContext)("PageContext", null);
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  export interface RouterContextType {
3
3
  navigateTo: (url: string) => void;
4
+ navigateToAsync?: (url: string) => Promise<void>;
4
5
  currentPage: string;
5
6
  currentUrl: string;
6
7
  params: Record<string, string>;
@@ -1,14 +1,20 @@
1
1
  import React from "react";
2
- export interface ArcanaJSAppProps {
2
+ export interface ArcanaJSAppProps<TData = any, TParams extends Record<string, string> = Record<string, string>> {
3
3
  initialPage: string;
4
- initialData: any;
5
- initialParams?: Record<string, string>;
4
+ initialData: TData;
5
+ initialParams?: TParams;
6
6
  initialUrl?: string;
7
7
  csrfToken?: string;
8
- views: Record<string, React.FC<any>>;
8
+ views: Record<string, React.ComponentType<{
9
+ data: TData;
10
+ navigateTo: (url: string) => Promise<void>;
11
+ params: TParams;
12
+ }>>;
9
13
  layout?: React.FC<{
10
14
  children: React.ReactNode;
11
15
  }>;
12
16
  onNavigate?: (url: string) => void;
17
+ /** Maximum number of entries to keep in the navigation cache */
18
+ cacheLimit?: number;
13
19
  }
14
- export declare const ArcanaJSApp: React.FC<ArcanaJSAppProps>;
20
+ export declare const ArcanaJSApp: <TData = any, TParams extends Record<string, string> = Record<string, string>>(props: ArcanaJSAppProps<TData, TParams>) => import("react/jsx-runtime").JSX.Element;
@@ -38,15 +38,18 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const react_1 = __importStar(require("react"));
39
39
  const Page_1 = require("../components/Page");
40
40
  const RouterContext_1 = require("../context/RouterContext");
41
- const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl, csrfToken, views, layout: Layout, onNavigate, }) => {
41
+ const ArcanaJSApp = (props) => {
42
+ const { initialPage, initialData, initialParams = {}, initialUrl, csrfToken, views, layout: Layout, onNavigate, cacheLimit = 50, } = props;
42
43
  const [page, setPage] = (0, react_1.useState)(initialPage);
43
44
  const [data, setData] = (0, react_1.useState)(initialData);
44
45
  const [params, setParams] = (0, react_1.useState)(initialParams);
45
46
  const [url, setUrl] = (0, react_1.useState)(initialUrl ||
46
47
  (typeof window !== "undefined" ? window.location.pathname : "/"));
47
48
  const [isNavigating, setIsNavigating] = (0, react_1.useState)(false);
48
- // Navigation cache to store previously visited pages
49
+ // Navigation cache to store previously visited pages (LRU via Map ordering)
49
50
  const navigationCache = react_1.default.useRef(new Map());
51
+ // Abort controller for in-flight navigation fetch
52
+ const currentAbort = react_1.default.useRef(null);
50
53
  (0, react_1.useEffect)(() => {
51
54
  if (typeof window !== "undefined" && !window.history.state) {
52
55
  window.history.replaceState({ page: initialPage, data: initialData, params: initialParams }, "", window.location.href);
@@ -59,36 +62,62 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
59
62
  setUrl(window.location.pathname);
60
63
  }
61
64
  else {
62
- window.location.reload();
65
+ // Try to fetch the page state instead of hard reload
66
+ const path = window.location.pathname;
67
+ void navigateTo(path).catch(() => {
68
+ window.location.reload();
69
+ });
63
70
  }
64
71
  };
65
72
  window.addEventListener("popstate", handlePopState);
66
- return () => window.removeEventListener("popstate", handlePopState);
73
+ return () => {
74
+ window.removeEventListener("popstate", handlePopState);
75
+ currentAbort.current?.abort();
76
+ };
67
77
  }, []);
78
+ const setCache = (key, value) => {
79
+ const map = navigationCache.current;
80
+ if (map.has(key))
81
+ map.delete(key);
82
+ map.set(key, value);
83
+ if (map.size > cacheLimit) {
84
+ const firstKey = map.keys().next().value;
85
+ if (firstKey !== undefined)
86
+ map.delete(firstKey);
87
+ }
88
+ };
68
89
  const navigateTo = async (newUrl) => {
69
90
  // Check cache first for instant navigation
70
- if (navigationCache.current.has(newUrl)) {
71
- const cached = navigationCache.current.get(newUrl);
91
+ const map = navigationCache.current;
92
+ if (map.has(newUrl)) {
93
+ const cached = map.get(newUrl);
72
94
  setPage(cached.page);
73
95
  setData(cached.data);
74
96
  setParams(cached.params || {});
75
97
  setUrl(newUrl);
76
98
  window.history.pushState(cached, "", newUrl);
77
- // Scroll to top
78
99
  if (typeof window !== "undefined") {
79
- window.scrollTo({ top: 0, behavior: "smooth" });
100
+ try {
101
+ window.scrollTo({ top: 0, behavior: "smooth" });
102
+ }
103
+ catch {
104
+ // ignore in non-browser env or when smooth not supported
105
+ }
80
106
  }
81
- if (onNavigate) {
107
+ if (onNavigate)
82
108
  onNavigate(newUrl);
83
- }
84
109
  return;
85
110
  }
86
111
  setIsNavigating(true);
112
+ // Abort previous request
113
+ currentAbort.current?.abort();
114
+ const controller = new AbortController();
115
+ currentAbort.current = controller;
87
116
  try {
88
117
  const response = await fetch(newUrl, {
89
118
  headers: { "X-ArcanaJS-Request": "true" },
90
- // prevent caching in dev navigation
91
119
  cache: "no-store",
120
+ signal: controller.signal,
92
121
  });
93
122
  if (!response.ok) {
94
123
  if (response.status === 404) {
@@ -99,49 +128,61 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
99
128
  }
100
129
  throw new Error(`Navigation failed: ${response.status} ${response.statusText}`);
101
130
  }
102
- // Ensure server returned JSON. If not, fallback to full navigation reload
103
131
  const contentType = response.headers.get("content-type") || "";
104
132
  if (!contentType.includes("application/json")) {
105
- // The server returned HTML (or something else) instead of JSON.
106
- // Do a full reload so the browser displays the correct page instead
107
- // of trying to parse HTML as JSON (which causes the SyntaxError).
108
133
  window.location.href = newUrl;
109
134
  return;
110
135
  }
111
136
  const json = await response.json();
112
- // Cache the navigation result
113
- navigationCache.current.set(newUrl, {
137
+ const payload = {
114
138
  page: json.page,
115
139
  data: json.data,
116
- params: json.params,
117
- });
118
- window.history.pushState({ page: json.page, data: json.data, params: json.params }, "", newUrl);
119
- setPage(json.page);
120
- setData(json.data);
121
- setParams(json.params || {});
140
+ params: (json.params || {}),
141
+ };
142
+ setCache(newUrl, payload);
143
+ window.history.pushState({ page: payload.page, data: payload.data, params: payload.params }, "", newUrl);
144
+ setPage(payload.page);
145
+ setData(payload.data);
146
+ setParams(payload.params || {});
122
147
  setUrl(newUrl);
123
- // Scroll to top after navigation
124
148
  if (typeof window !== "undefined") {
125
- window.scrollTo({ top: 0, behavior: "smooth" });
149
+ try {
150
+ window.scrollTo({ top: 0, behavior: "smooth" });
151
+ }
152
+ catch {
153
+ // ignore
154
+ }
126
155
  }
127
- if (onNavigate) {
156
+ if (onNavigate)
128
157
  onNavigate(newUrl);
129
- }
130
158
  }
131
- catch (error) {
132
- console.error("Navigation failed", error);
159
+ catch (err) {
160
+ if (err?.name === "AbortError")
161
+ return;
162
+ console.error("Navigation failed", err);
163
+ throw err;
133
164
  }
134
165
  finally {
166
+ // Clear abort controller if it's still the one we set
167
+ if (currentAbort.current === controller)
168
+ currentAbort.current = null;
135
169
  setIsNavigating(false);
136
170
  }
137
171
  };
138
172
  const renderPage = () => {
139
- const Component = views[page] || views["NotFoundPage"] || (() => (0, jsx_runtime_1.jsx)("div", { children: "404 Not Found" }));
173
+ const Component = (views[page] ||
174
+ views["NotFoundPage"] ||
175
+ (() => (0, jsx_runtime_1.jsx)("div", { children: "404 Not Found" })));
140
176
  return ((0, jsx_runtime_1.jsx)(Page_1.Page, { data: data, children: (0, jsx_runtime_1.jsx)(Component, { data: data, navigateTo: navigateTo, params: params }) }));
141
177
  };
142
178
  const content = renderPage();
143
179
  return ((0, jsx_runtime_1.jsx)(RouterContext_1.RouterProvider, { value: {
144
- navigateTo,
180
+ // keep backward-compatible wrapper that doesn't return a promise
181
+ navigateTo: (...args) => {
182
+ void navigateTo(args[0]);
183
+ },
184
+ // new async API consumers can use `navigateToAsync` when available
185
+ navigateToAsync: navigateTo,
145
186
  currentPage: page,
146
187
  currentUrl: url,
147
188
  params,
@@ -8,7 +8,7 @@
8
8
  "start": "arcanajs start"
9
9
  },
10
10
  "dependencies": {
11
- "arcanajs": "^2.5.3",
11
+ "arcanajs": "^2.6.0",
12
12
  "react": "^19.2.0",
13
13
  "react-dom": "^19.2.0"
14
14
  }
@@ -1,7 +1,19 @@
1
1
  import { Request, Response } from "arcanajs/server";
2
+ import type { HomePageData } from "../../types/HomePageData";
2
3
 
3
4
  export default class HomeController {
4
5
  home(_req: Request, res: Response) {
5
- res.renderPage("HomePage");
6
+ // Provide example page data to demonstrate passing data to the view
7
+ const data: HomePageData = {
8
+ welcome: "Welcome to ArcanaJS",
9
+ subtitle: "A modern React framework with server-side rendering",
10
+ time: new Date().toISOString(),
11
+ posts: [
12
+ { id: 1, title: "Getting started with ArcanaJS" },
13
+ { id: 2, title: "Building fast apps with SSR" },
14
+ ],
15
+ };
16
+
17
+ res.renderPage("HomePage", data);
6
18
  }
7
19
  }
@@ -20,10 +20,13 @@ const server = new ArcanaJSServer({
20
20
  // Example: provide a dbConnect function that returns the DB client/connection.
21
21
  // You can connect to MySQL/Postgres/MongoDB here and return the client.
22
22
  // dbConnect: async () => {
23
- // const { MongoClient } = await import('mongodb');
24
- // const client = new MongoClient(process.env.MONGO_URL || 'mongodb://localhost:27017');
25
- // await client.connect();
26
- // return client.db('mydb');
23
+ // const { MongoClient } = await import("mongodb");
24
+ // const url = process.env.MONGO_URL || "mongodb://localhost:27017";
25
+ // const dbName = process.env.MONGO_DB || "mydb";
26
+ // const client = new MongoClient(url);
27
+ // await client.connect();
28
+ // const db = client.db(dbName);
29
+ // return { client, db };
27
30
  // },
28
31
  // Or use one of the provided DB templates (uncomment one):
29
32
  // dbConnect: mongoDb,
@@ -0,0 +1,11 @@
1
+ export interface Post {
2
+ id: number;
3
+ title: string;
4
+ }
5
+
6
+ export interface HomePageData {
7
+ welcome: string;
8
+ subtitle?: string;
9
+ time?: string;
10
+ posts?: Post[];
11
+ }
@@ -1,6 +1,9 @@
1
- import { Body, Head, Page } from "arcanajs";
1
+ import { Body, Head, Page, usePage } from "arcanajs";
2
+ import type { HomePageData } from "../types/HomePageData";
2
3
 
3
4
  export default function HomePage() {
5
+ const pageData = usePage<HomePageData | null>();
6
+
4
7
  return (
5
8
  <Page>
6
9
  <Head>
@@ -83,6 +86,28 @@ export default function HomePage() {
83
86
  Tailwind CSS v4 support. Build fast, beautiful applications
84
87
  with zero configuration.
85
88
  </p>
89
+ {pageData && (
90
+ <div className="text-sm text-gray-400 mb-6">
91
+ <div className="font-semibold text-white">
92
+ {pageData.welcome ?? "Welcome"}
93
+ </div>
94
+ <div className="text-gray-400 text-sm">
95
+ {pageData.subtitle}
96
+ </div>
97
+ <div className="text-xs text-gray-500 mt-1">
98
+ Server time: {pageData.time}
99
+ </div>
100
+ {Array.isArray(pageData.posts) && (
101
+ <ul className="mt-3 text-left list-disc list-inside text-gray-300">
102
+ {pageData.posts.map((p) => (
103
+ <li key={p.id} className="truncate">
104
+ {p.title}
105
+ </li>
106
+ ))}
107
+ </ul>
108
+ )}
109
+ </div>
110
+ )}
86
111
  <div className="flex flex-col sm:flex-row gap-4 justify-center">
87
112
  <button className="btn-primary px-8 py-3.5 text-lg font-semibold rounded-xl inline-flex items-center justify-center gap-2">
88
113
  Get Started
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "email": "mohammed.bencheikh.dev@gmail.com",
6
6
  "url": "https://mohammedbencheikh.com/"
7
7
  },
8
- "version": "2.5.3",
8
+ "version": "2.6.0",
9
9
  "description": "ArcanaJS Framework",
10
10
  "main": "framework/lib/index.js",
11
11
  "types": "framework/lib/index.d.ts",