arcanajs 2.3.6 → 2.4.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/framework/cli/templates.js +1 -1
- package/framework/lib/client/index.d.ts +26 -1
- package/framework/lib/client/index.js +17 -2
- package/framework/lib/shared/components/Link.d.ts +1 -0
- package/framework/lib/shared/components/Link.js +16 -3
- package/framework/lib/shared/components/NavLink.d.ts +1 -0
- package/framework/lib/shared/components/NavLink.js +3 -3
- package/framework/lib/shared/context/RouterContext.d.ts +2 -0
- package/framework/lib/shared/core/ArcanaJSApp.d.ts +1 -0
- package/framework/lib/shared/core/ArcanaJSApp.js +80 -3
- package/framework/templates/package.json +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ exports.configFiles = [
|
|
|
6
6
|
{ src: "tsconfig.json", dest: "tsconfig.json" },
|
|
7
7
|
{ src: "arcanajs.config.ts", dest: "arcanajs.config.ts" },
|
|
8
8
|
{ src: "postcss.config.js", dest: "postcss.config.js" },
|
|
9
|
-
{ src: "arcanajs.d.ts", dest: "arcanajs.d.ts" },
|
|
9
|
+
{ src: "arcanajs.d.ts", dest: "src/arcanajs.d.ts" },
|
|
10
10
|
{ src: "globals.css", dest: "src/client/globals.css" },
|
|
11
11
|
{ src: "client-index.tsx", dest: "src/client/index.tsx" },
|
|
12
12
|
{ src: "server-index.ts", dest: "src/server/index.ts" },
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
export type { LayoutComponent, ViewsRegistry } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Navigation options for configuring ArcanaJS behavior
|
|
5
|
+
*/
|
|
6
|
+
export interface NavigationOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Callback function called after each successful navigation
|
|
9
|
+
* Useful for analytics, logging, or custom scroll behavior
|
|
10
|
+
*/
|
|
11
|
+
onNavigate?: (url: string) => void;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Hydrate the ArcanaJS application on the client side
|
|
5
15
|
*
|
|
@@ -8,6 +18,7 @@ export type { LayoutComponent, ViewsRegistry } from "../types";
|
|
|
8
18
|
*
|
|
9
19
|
* @param viewsOrContext - Either a views registry object or a webpack require.context
|
|
10
20
|
* @param layout - Optional layout component to wrap all pages
|
|
21
|
+
* @param options - Optional navigation configuration options
|
|
11
22
|
*
|
|
12
23
|
* @example
|
|
13
24
|
* ```typescript
|
|
@@ -30,5 +41,19 @@ export type { LayoutComponent, ViewsRegistry } from "../types";
|
|
|
30
41
|
* AboutPage,
|
|
31
42
|
* });
|
|
32
43
|
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // With navigation options
|
|
48
|
+
* import { hydrateArcanaJS } from 'arcanajs/client';
|
|
49
|
+
*
|
|
50
|
+
* const views = require.context('./views', false, /\.tsx$/);
|
|
51
|
+
* hydrateArcanaJS(views, undefined, {
|
|
52
|
+
* onNavigate: (url) => {
|
|
53
|
+
* // Track page views
|
|
54
|
+
* gtag('event', 'page_view', { page_path: url });
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
33
58
|
*/
|
|
34
|
-
export declare const hydrateArcanaJS: (viewsOrContext: Record<string, React.FC<any>> | any, layout?: React.FC<any
|
|
59
|
+
export declare const hydrateArcanaJS: (viewsOrContext: Record<string, React.FC<any>> | any, layout?: React.FC<any>, options?: NavigationOptions) => void;
|
|
@@ -21,6 +21,7 @@ const NotFoundPage_1 = __importDefault(require("../shared/views/NotFoundPage"));
|
|
|
21
21
|
*
|
|
22
22
|
* @param viewsOrContext - Either a views registry object or a webpack require.context
|
|
23
23
|
* @param layout - Optional layout component to wrap all pages
|
|
24
|
+
* @param options - Optional navigation configuration options
|
|
24
25
|
*
|
|
25
26
|
* @example
|
|
26
27
|
* ```typescript
|
|
@@ -43,8 +44,22 @@ const NotFoundPage_1 = __importDefault(require("../shared/views/NotFoundPage"));
|
|
|
43
44
|
* AboutPage,
|
|
44
45
|
* });
|
|
45
46
|
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // With navigation options
|
|
51
|
+
* import { hydrateArcanaJS } from 'arcanajs/client';
|
|
52
|
+
*
|
|
53
|
+
* const views = require.context('./views', false, /\.tsx$/);
|
|
54
|
+
* hydrateArcanaJS(views, undefined, {
|
|
55
|
+
* onNavigate: (url) => {
|
|
56
|
+
* // Track page views
|
|
57
|
+
* gtag('event', 'page_view', { page_path: url });
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
46
61
|
*/
|
|
47
|
-
const hydrateArcanaJS = (viewsOrContext, layout) => {
|
|
62
|
+
const hydrateArcanaJS = (viewsOrContext, layout, options) => {
|
|
48
63
|
let views = {};
|
|
49
64
|
if (viewsOrContext.keys && typeof viewsOrContext.keys === "function") {
|
|
50
65
|
viewsOrContext.keys().forEach((key) => {
|
|
@@ -72,7 +87,7 @@ const hydrateArcanaJS = (viewsOrContext, layout) => {
|
|
|
72
87
|
if (container && dataScript) {
|
|
73
88
|
try {
|
|
74
89
|
const { page, data, params, csrfToken } = JSON.parse(dataScript.textContent || "{}");
|
|
75
|
-
(0, client_1.hydrateRoot)(container, (0, jsx_runtime_1.jsx)(HeadContext_1.HeadContext.Provider, { value: headManager, children: (0, jsx_runtime_1.jsx)(ArcanaJSApp_1.ArcanaJSApp, { initialPage: page, initialData: data, initialParams: params, csrfToken: csrfToken, views: views, layout: layout }) }));
|
|
90
|
+
(0, client_1.hydrateRoot)(container, (0, jsx_runtime_1.jsx)(HeadContext_1.HeadContext.Provider, { value: headManager, children: (0, jsx_runtime_1.jsx)(ArcanaJSApp_1.ArcanaJSApp, { initialPage: page, initialData: data, initialParams: params, csrfToken: csrfToken, views: views, layout: layout, onNavigate: options?.onNavigate || (() => { }) }) }));
|
|
76
91
|
}
|
|
77
92
|
catch (e) {
|
|
78
93
|
console.error("Failed to parse initial data", e);
|
|
@@ -3,12 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Link = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const useRouter_1 = require("../hooks/useRouter");
|
|
6
|
-
const Link = ({ href, children, ...props }) => {
|
|
6
|
+
const Link = ({ href, children, prefetch = false, ...props }) => {
|
|
7
7
|
const { navigateTo } = (0, useRouter_1.useRouter)();
|
|
8
|
+
const isExternal = /^https?:\/\//.test(href);
|
|
8
9
|
const handleClick = (e) => {
|
|
9
10
|
e.preventDefault();
|
|
10
|
-
|
|
11
|
+
if (!isExternal) {
|
|
12
|
+
navigateTo(href);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Open external links in a new tab
|
|
16
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
17
|
+
}
|
|
11
18
|
};
|
|
12
|
-
|
|
19
|
+
const handleMouseEnter = () => {
|
|
20
|
+
if (prefetch && !isExternal) {
|
|
21
|
+
// Prefetch using HEAD request to warm cache
|
|
22
|
+
fetch(href, { method: "HEAD" }).catch(() => { });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return ((0, jsx_runtime_1.jsx)("a", { href: href, onClick: handleClick, onMouseEnter: handleMouseEnter, target: isExternal ? "_blank" : undefined, rel: isExternal ? "noopener noreferrer" : undefined, ...props, children: children }));
|
|
13
26
|
};
|
|
14
27
|
exports.Link = Link;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NavLink = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
const Link_1 = require("./Link");
|
|
6
5
|
const useRouter_1 = require("../hooks/useRouter");
|
|
7
|
-
const
|
|
6
|
+
const Link_1 = require("./Link");
|
|
7
|
+
const NavLink = ({ href, activeClassName = "active", className = "", exact = false, prefetch = false, children, ...props }) => {
|
|
8
8
|
const { currentUrl } = (0, useRouter_1.useRouter)();
|
|
9
9
|
const isActive = exact ? currentUrl === href : currentUrl.startsWith(href);
|
|
10
10
|
const combinedClassName = `${className} ${isActive ? activeClassName : ""}`.trim();
|
|
11
|
-
return ((0, jsx_runtime_1.jsx)(Link_1.Link, { href: href, className: combinedClassName, ...props, children: children }));
|
|
11
|
+
return ((0, jsx_runtime_1.jsx)(Link_1.Link, { href: href, className: combinedClassName, prefetch: prefetch, ...props, children: children }));
|
|
12
12
|
};
|
|
13
13
|
exports.NavLink = NavLink;
|
|
@@ -5,6 +5,8 @@ export interface RouterContextType {
|
|
|
5
5
|
currentUrl: string;
|
|
6
6
|
params: Record<string, string>;
|
|
7
7
|
csrfToken?: string;
|
|
8
|
+
onNavigate?: (url: string) => void;
|
|
9
|
+
isNavigating: boolean;
|
|
8
10
|
}
|
|
9
11
|
export declare const RouterContext: React.Context<RouterContextType | null>;
|
|
10
12
|
export declare const RouterProvider: React.FC<{
|
|
@@ -1,17 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.ArcanaJSApp = void 0;
|
|
4
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
const react_1 = require("react");
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
6
39
|
const Page_1 = require("../components/Page");
|
|
7
40
|
const RouterContext_1 = require("../context/RouterContext");
|
|
8
|
-
const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl, csrfToken, views, layout: Layout, }) => {
|
|
41
|
+
const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl, csrfToken, views, layout: Layout, onNavigate, }) => {
|
|
9
42
|
const [page, setPage] = (0, react_1.useState)(initialPage);
|
|
10
43
|
const [data, setData] = (0, react_1.useState)(initialData);
|
|
11
44
|
const [params, setParams] = (0, react_1.useState)(initialParams);
|
|
12
45
|
const [url, setUrl] = (0, react_1.useState)(initialUrl ||
|
|
13
46
|
(typeof window !== "undefined" ? window.location.pathname : "/"));
|
|
47
|
+
const [isNavigating, setIsNavigating] = (0, react_1.useState)(false);
|
|
48
|
+
// Navigation cache to store previously visited pages
|
|
49
|
+
const navigationCache = react_1.default.useRef(new Map());
|
|
14
50
|
(0, react_1.useEffect)(() => {
|
|
51
|
+
if (typeof window !== "undefined" && !window.history.state) {
|
|
52
|
+
window.history.replaceState({ page: initialPage, data: initialData, params: initialParams }, "", window.location.href);
|
|
53
|
+
}
|
|
15
54
|
const handlePopState = (event) => {
|
|
16
55
|
if (event.state) {
|
|
17
56
|
setPage(event.state.page);
|
|
@@ -19,11 +58,32 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
|
|
|
19
58
|
setParams(event.state.params || {});
|
|
20
59
|
setUrl(window.location.pathname);
|
|
21
60
|
}
|
|
61
|
+
else {
|
|
62
|
+
window.location.reload();
|
|
63
|
+
}
|
|
22
64
|
};
|
|
23
65
|
window.addEventListener("popstate", handlePopState);
|
|
24
66
|
return () => window.removeEventListener("popstate", handlePopState);
|
|
25
67
|
}, []);
|
|
26
68
|
const navigateTo = async (newUrl) => {
|
|
69
|
+
// Check cache first for instant navigation
|
|
70
|
+
if (navigationCache.current.has(newUrl)) {
|
|
71
|
+
const cached = navigationCache.current.get(newUrl);
|
|
72
|
+
setPage(cached.page);
|
|
73
|
+
setData(cached.data);
|
|
74
|
+
setParams(cached.params || {});
|
|
75
|
+
setUrl(newUrl);
|
|
76
|
+
window.history.pushState(cached, "", newUrl);
|
|
77
|
+
// Scroll to top
|
|
78
|
+
if (typeof window !== "undefined") {
|
|
79
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
80
|
+
}
|
|
81
|
+
if (onNavigate) {
|
|
82
|
+
onNavigate(newUrl);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setIsNavigating(true);
|
|
27
87
|
try {
|
|
28
88
|
const response = await fetch(newUrl, {
|
|
29
89
|
headers: { "x-arcanajs-request": "true" },
|
|
@@ -38,15 +98,30 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
|
|
|
38
98
|
throw new Error(`Navigation failed: ${response.status} ${response.statusText}`);
|
|
39
99
|
}
|
|
40
100
|
const json = await response.json();
|
|
101
|
+
// Cache the navigation result
|
|
102
|
+
navigationCache.current.set(newUrl, {
|
|
103
|
+
page: json.page,
|
|
104
|
+
data: json.data,
|
|
105
|
+
params: json.params,
|
|
106
|
+
});
|
|
41
107
|
window.history.pushState({ page: json.page, data: json.data, params: json.params }, "", newUrl);
|
|
42
108
|
setPage(json.page);
|
|
43
109
|
setData(json.data);
|
|
44
110
|
setParams(json.params || {});
|
|
45
111
|
setUrl(newUrl);
|
|
112
|
+
// Scroll to top after navigation
|
|
113
|
+
if (typeof window !== "undefined") {
|
|
114
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
115
|
+
}
|
|
116
|
+
if (onNavigate) {
|
|
117
|
+
onNavigate(newUrl);
|
|
118
|
+
}
|
|
46
119
|
}
|
|
47
120
|
catch (error) {
|
|
48
121
|
console.error("Navigation failed", error);
|
|
49
|
-
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
setIsNavigating(false);
|
|
50
125
|
}
|
|
51
126
|
};
|
|
52
127
|
const renderPage = () => {
|
|
@@ -60,6 +135,8 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
|
|
|
60
135
|
currentUrl: url,
|
|
61
136
|
params,
|
|
62
137
|
csrfToken,
|
|
138
|
+
onNavigate,
|
|
139
|
+
isNavigating,
|
|
63
140
|
}, children: Layout ? (0, jsx_runtime_1.jsx)(Layout, { children: content }) : (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: content }) }));
|
|
64
141
|
};
|
|
65
142
|
exports.ArcanaJSApp = ArcanaJSApp;
|
package/package.json
CHANGED