arcanajs 2.4.0 → 2.5.1

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.
Files changed (27) hide show
  1. package/framework/cli/templates.js +26 -11
  2. package/framework/lib/global.d.ts +7 -0
  3. package/framework/lib/server/ArcanaJSMiddleware.js +1 -1
  4. package/framework/lib/server/ArcanaJSServer.d.ts +36 -4
  5. package/framework/lib/server/ArcanaJSServer.js +319 -43
  6. package/framework/lib/shared/core/ArcanaJSApp.js +12 -1
  7. package/framework/templates/package.json +1 -1
  8. package/framework/templates/{globals.css → src/client/globals.css} +5 -4
  9. package/framework/templates/src/db/mongo.ts +10 -0
  10. package/framework/templates/src/db/mongoose.ts +12 -0
  11. package/framework/templates/src/db/mysql.ts +15 -0
  12. package/framework/templates/src/db/postgres.ts +8 -0
  13. package/framework/templates/src/server/controllers/UsersController.ts +37 -0
  14. package/framework/templates/src/server/index.ts +35 -0
  15. package/framework/templates/src/server/routes/api.ts +6 -0
  16. package/framework/templates/{HomePage.tsx → src/views/HomePage.tsx} +1 -1
  17. package/package.json +1 -1
  18. package/framework/templates/server-index.ts +0 -11
  19. /package/framework/templates/{arcanajs.png → public/arcanajs.png} +0 -0
  20. /package/framework/templates/{arcanajs.svg → public/arcanajs.svg} +0 -0
  21. /package/framework/templates/{favicon.ico → public/favicon.ico} +0 -0
  22. /package/framework/templates/{arcanajs.d.ts → src/arcanajs.d.ts} +0 -0
  23. /package/framework/templates/{client-index.tsx → src/client/index.tsx} +0 -0
  24. /package/framework/templates/{server-controller-home.ts → src/server/controllers/HomeController.ts} +0 -0
  25. /package/framework/templates/{server-routes-web.ts → src/server/routes/web.ts} +0 -0
  26. /package/framework/templates/{ErrorPage.tsx → src/views/ErrorPage.tsx} +0 -0
  27. /package/framework/templates/{NotFoundPage.tsx → src/views/NotFoundPage.tsx} +0 -0
@@ -2,35 +2,50 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requiredDirs = exports.errorPages = exports.configFiles = void 0;
4
4
  exports.configFiles = [
5
+ // project root files
5
6
  { src: "package.json", dest: "package.json" },
6
7
  { src: "tsconfig.json", dest: "tsconfig.json" },
7
8
  { src: "arcanajs.config.ts", dest: "arcanajs.config.ts" },
8
9
  { src: "postcss.config.js", dest: "postcss.config.js" },
9
- { src: "arcanajs.d.ts", dest: "src/arcanajs.d.ts" },
10
- { src: "globals.css", dest: "src/client/globals.css" },
11
- { src: "client-index.tsx", dest: "src/client/index.tsx" },
12
- { src: "server-index.ts", dest: "src/server/index.ts" },
13
- { src: "server-routes-web.ts", dest: "src/server/routes/web.ts" },
10
+ // types
11
+ { src: "src/arcanajs.d.ts", dest: "src/arcanajs.d.ts" },
12
+ // client
13
+ { src: "src/client/globals.css", dest: "src/client/globals.css" },
14
+ { src: "src/client/index.tsx", dest: "src/client/index.tsx" },
15
+ // server
16
+ { src: "src/server/index.ts", dest: "src/server/index.ts" },
17
+ { src: "src/server/routes/web.ts", dest: "src/server/routes/web.ts" },
18
+ { src: "src/server/routes/api.ts", dest: "src/server/routes/api.ts" },
14
19
  {
15
- src: "server-controller-home.ts",
20
+ src: "src/server/controllers/HomeController.ts",
16
21
  dest: "src/server/controllers/HomeController.ts",
17
22
  },
18
23
  {
19
- src: "HomePage.tsx",
20
- dest: "src/views/HomePage.tsx",
24
+ src: "src/server/controllers/UsersController.ts",
25
+ dest: "src/server/controllers/UsersController.ts",
21
26
  },
27
+ // views
28
+ { src: "src/views/HomePage.tsx", dest: "src/views/HomePage.tsx" },
29
+ { src: "src/views/NotFoundPage.tsx", dest: "src/views/NotFoundPage.tsx" },
30
+ { src: "src/views/ErrorPage.tsx", dest: "src/views/ErrorPage.tsx" },
31
+ //public
22
32
  {
23
- src: "arcanajs.png",
33
+ src: "public/arcanajs.png",
24
34
  dest: "public/arcanajs.png",
25
35
  },
26
36
  {
27
- src: "arcanajs.svg",
37
+ src: "public/arcanajs.svg",
28
38
  dest: "public/arcanajs.svg",
29
39
  },
30
40
  {
31
- src: "favicon.ico",
41
+ src: "public/favicon.ico",
32
42
  dest: "public/favicon.ico",
33
43
  },
44
+ // optional DB templates
45
+ { src: "src/db/mongo.ts", dest: "src/db/mongo.ts" },
46
+ { src: "src/db/mongoose.ts", dest: "src/db/mongoose.ts" },
47
+ { src: "src/db/mysql.ts", dest: "src/db/mysql.ts" },
48
+ { src: "src/db/postgres.ts", dest: "src/db/postgres.ts" },
34
49
  ];
35
50
  exports.errorPages = ["NotFoundPage.tsx", "ErrorPage.tsx"];
36
51
  exports.requiredDirs = [
@@ -18,6 +18,13 @@ declare module "*.module.css" {
18
18
  declare global {
19
19
  var __non_webpack_require__: NodeJS.Require;
20
20
  namespace Express {
21
+ interface Request {
22
+ /**
23
+ * Normalized DB object optionally attached to the request by ArcanaJSServer.
24
+ * It may be either the raw client, or an object like `{ client, db, close }`.
25
+ */
26
+ db?: any;
27
+ }
21
28
  interface Response {
22
29
  /**
23
30
  * Render a page component with data
@@ -50,7 +50,7 @@ const createArcanaJSMiddleware = (options) => {
50
50
  return (req, res, next) => {
51
51
  res.renderPage = (page, data = {}, params = {}) => {
52
52
  const csrfToken = res.locals.csrfToken;
53
- if (req.headers["x-arcanajs-request"] || req.query.format === "json") {
53
+ if (req.get("X-ArcanaJS-Request") || req.query.format === "json") {
54
54
  return res.json({ page, data, params, csrfToken });
55
55
  }
56
56
  try {
@@ -1,23 +1,55 @@
1
1
  import { Express, RequestHandler } from "express";
2
2
  import React from "react";
3
- export interface ArcanaJSConfig {
3
+ import { Router as ArcanaRouter } from "./Router";
4
+ export interface ArcanaJSConfig<TDb = any> {
4
5
  port?: number | string;
5
6
  views?: Record<string, React.FC<any>>;
6
7
  viewsDir?: string;
7
8
  viewsContext?: any;
8
- routes?: RequestHandler | RequestHandler[];
9
+ routes?: RequestHandler | RequestHandler[] | ArcanaRouter | ArcanaRouter[];
10
+ /** API routes can be provided separately from web routes */
11
+ apiRoutes?: RequestHandler | RequestHandler[] | ArcanaRouter | ArcanaRouter[];
12
+ /** Base path under which API routes will be mounted (default: '/api') */
13
+ apiBase?: string;
9
14
  staticDir?: string;
10
15
  distDir?: string;
11
16
  indexFile?: string;
12
17
  layout?: React.FC<any>;
18
+ /** Optional function to establish a DB connection. Should return a Promise resolving to the DB client/connection. */
19
+ dbConnect?: () => Promise<TDb> | TDb;
20
+ /** Automatically register SIGINT/SIGTERM handlers to call stop(). Default: true */
21
+ autoHandleSignals?: boolean;
13
22
  }
14
- export declare class ArcanaJSServer {
23
+ declare global {
24
+ namespace Express {
25
+ interface Request {
26
+ /**
27
+ * Normalized DB object optionally attached to the request by ArcanaJSServer.
28
+ * It may be either the raw client, or an object like `{ client, db, close }`.
29
+ */
30
+ db?: any;
31
+ }
32
+ }
33
+ }
34
+ export declare class ArcanaJSServer<TDb = any> {
15
35
  app: Express;
16
36
  private config;
17
- constructor(config: ArcanaJSConfig);
37
+ private serverInstance?;
38
+ private _sigintHandler?;
39
+ private _sigtermHandler?;
40
+ constructor(config: ArcanaJSConfig<TDb>);
41
+ /**
42
+ * Normalize different DB client shapes into a single object exposing:
43
+ * { client?: any, db?: any, close: async () => void }
44
+ */
45
+ private normalizeDb;
18
46
  private initialize;
19
47
  private loadViewsFromContext;
20
48
  private loadViewsFromAlias;
21
49
  private discoverViews;
22
50
  start(): void;
51
+ /**
52
+ * Stop the HTTP server and close DB connection if present.
53
+ */
54
+ stop(): Promise<void>;
23
55
  }
@@ -22,71 +22,225 @@ class ArcanaJSServer {
22
22
  this.app = (0, express_1.default)();
23
23
  this.initialize();
24
24
  }
25
- initialize() {
26
- let { staticDir = "public", distDir = "dist/public", indexFile = "dist/public/index.html", views, viewsContext, routes, layout, } = this.config;
27
- // 1. Load views from config or context (highest priority)
28
- if (!views && viewsContext) {
29
- views = this.loadViewsFromContext(viewsContext);
25
+ /**
26
+ * Normalize different DB client shapes into a single object exposing:
27
+ * { client?: any, db?: any, close: async () => void }
28
+ */
29
+ normalizeDb(obj) {
30
+ if (!obj)
31
+ return obj;
32
+ // If already normalized (has close function), return as-is
33
+ if (typeof obj.close === "function") {
34
+ return obj;
35
+ }
36
+ // Mongoose instance
37
+ if (typeof obj.disconnect === "function") {
38
+ return {
39
+ client: obj,
40
+ close: async () => {
41
+ await obj.disconnect();
42
+ },
43
+ };
44
+ }
45
+ // If object contains { client, db }
46
+ if (obj.client && obj.db) {
47
+ const client = obj.client;
48
+ return {
49
+ client,
50
+ db: obj.db,
51
+ close: async () => {
52
+ if (client && typeof client.close === "function") {
53
+ await client.close();
54
+ }
55
+ else if (client && typeof client.disconnect === "function") {
56
+ await client.disconnect();
57
+ }
58
+ else {
59
+ throw new Error("No close method on client");
60
+ }
61
+ },
62
+ };
63
+ }
64
+ // Native MongoClient instance
65
+ if (obj && typeof obj.close === "function" && obj.connect) {
66
+ return {
67
+ client: obj,
68
+ close: async () => {
69
+ await obj.close();
70
+ },
71
+ };
30
72
  }
31
- // 2. Load views from injected alias (Webpack)
32
- if (!views) {
33
- views = this.loadViewsFromAlias();
73
+ // Pg/mysql client with end()/query()
74
+ if (typeof obj.end === "function" || typeof obj.query === "function") {
75
+ return {
76
+ client: obj,
77
+ close: async () => {
78
+ if (typeof obj.end === "function") {
79
+ await obj.end();
80
+ }
81
+ else if (typeof obj.close === "function") {
82
+ await obj.close();
83
+ }
84
+ else {
85
+ throw new Error("No close/end method on SQL client");
86
+ }
87
+ },
88
+ };
34
89
  }
35
- // 3. Fallback to auto-discovery (Server-side only, non-bundled)
36
- if (!views) {
37
- views = this.discoverViews();
90
+ // Try internal mongo client path { s: { client } }
91
+ if (obj.s && obj.s.client && typeof obj.s.client.close === "function") {
92
+ return {
93
+ client: obj.s.client,
94
+ db: obj,
95
+ close: async () => {
96
+ await obj.s.client.close();
97
+ },
98
+ };
38
99
  }
39
- if (!views || Object.keys(views).length === 0) {
100
+ // Fallback: wrap with a close that throws to surface the issue
101
+ return {
102
+ client: obj,
103
+ close: async () => {
104
+ throw new Error("No known close method on DB client");
105
+ },
106
+ };
107
+ }
108
+ initialize() {
109
+ const { staticDir = "public", distDir = "dist/public", indexFile = "dist/public/index.html", views, viewsContext, routes, layout, apiRoutes, apiBase = "/api", } = this.config;
110
+ const root = process.cwd();
111
+ // 1. Resolve views once and in priority order
112
+ let resolvedViews = views;
113
+ if (!resolvedViews && viewsContext)
114
+ resolvedViews = this.loadViewsFromContext(viewsContext);
115
+ if (!resolvedViews)
116
+ resolvedViews = this.loadViewsFromAlias();
117
+ if (!resolvedViews)
118
+ resolvedViews = this.discoverViews();
119
+ if (!resolvedViews || Object.keys(resolvedViews).length === 0) {
40
120
  console.warn("No views found. Please check your views directory.");
41
- views = {};
42
- }
43
- // Add default error views if not already present
44
- views.NotFoundPage = views.NotFoundPage || NotFoundPage_1.default;
45
- views.ErrorPage = views.ErrorPage || ErrorPage_1.default;
46
- // Security and Performance
47
- this.app.use((0, helmet_1.default)({
48
- contentSecurityPolicy: false,
49
- }));
50
- this.app.use((0, compression_1.default)());
121
+ resolvedViews = {};
122
+ }
123
+ resolvedViews.NotFoundPage = resolvedViews.NotFoundPage || NotFoundPage_1.default;
124
+ resolvedViews.ErrorPage = resolvedViews.ErrorPage || ErrorPage_1.default;
125
+ // Security headers
126
+ this.app.use((0, helmet_1.default)({ contentSecurityPolicy: false }));
51
127
  this.app.use((0, cookie_parser_1.default)());
52
128
  this.app.use((0, CsrfMiddleware_1.createCsrfMiddleware)());
53
129
  this.app.use(ResponseHandlerMiddleware_1.responseHandler);
54
- // Static files
130
+ // Expose `req.db` for convenience
131
+ this.app.use((req, _res, next) => {
132
+ req.db = this.app.locals.db;
133
+ next();
134
+ });
135
+ // Static files: resolve and dedupe paths, serve before compression to avoid recompressing static files
55
136
  const isProduction = process.env.NODE_ENV === "production";
56
- const staticOptions = {
57
- index: false,
58
- maxAge: isProduction ? "1y" : "0",
59
- };
60
- this.app.use(express_1.default.static(path_1.default.resolve(process.cwd(), distDir), staticOptions));
61
- this.app.use(express_1.default.static(path_1.default.resolve(process.cwd(), staticDir), staticOptions));
137
+ const staticOptions = { index: false, maxAge: isProduction ? "1y" : "0" };
138
+ const staticPaths = [
139
+ path_1.default.resolve(root, distDir),
140
+ path_1.default.resolve(root, staticDir),
141
+ ].filter((p, i, a) => a.indexOf(p) === i);
142
+ for (const p of staticPaths) {
143
+ this.app.use(express_1.default.static(p, staticOptions));
144
+ }
145
+ // Compression for dynamic responses (after static middleware)
146
+ this.app.use((0, compression_1.default)());
62
147
  // ArcanaJS Middleware
63
148
  this.app.use((0, ArcanaJSMiddleware_1.createArcanaJSMiddleware)({
64
- views,
65
- indexFile: path_1.default.resolve(process.cwd(), indexFile),
149
+ views: resolvedViews,
150
+ indexFile: path_1.default.resolve(root, indexFile),
66
151
  layout,
67
152
  }));
68
- // Custom Routes
69
- if (routes) {
70
- if (Array.isArray(routes)) {
71
- routes.forEach((route) => this.app.use(route));
153
+ // Establish DB connection if provided (normalize eagerly where possible)
154
+ if (this.config.dbConnect) {
155
+ try {
156
+ const maybe = this.config.dbConnect();
157
+ const handleDb = (db) => {
158
+ try {
159
+ this.app.locals.db = this.normalizeDb(db) || db;
160
+ console.log("Database connection attached to app.locals.db");
161
+ }
162
+ catch (e) {
163
+ this.app.locals.db = db;
164
+ console.warn("DB connection attached without full normalization", e);
165
+ }
166
+ };
167
+ if (maybe &&
168
+ maybe.then &&
169
+ typeof maybe.then === "function") {
170
+ maybe
171
+ .then(handleDb)
172
+ .catch((err) => console.error("Error establishing DB connection:", err));
173
+ }
174
+ else {
175
+ handleDb(maybe);
176
+ }
72
177
  }
73
- else {
74
- this.app.use(routes);
178
+ catch (err) {
179
+ console.error("Error calling dbConnect:", err);
180
+ }
181
+ }
182
+ // Helper to mount arrays or single route objects
183
+ const mount = (target, base) => {
184
+ if (!target)
185
+ return;
186
+ const items = Array.isArray(target) ? target : [target];
187
+ for (const r of items) {
188
+ if (!r)
189
+ continue;
190
+ if (typeof r.getRouter === "function") {
191
+ this.app.use(base || "/", r.getRouter());
192
+ }
193
+ else {
194
+ this.app.use(base || "/", r);
195
+ }
75
196
  }
197
+ };
198
+ try {
199
+ mount(apiRoutes, apiBase);
200
+ if (apiRoutes)
201
+ console.log(`API routes mounted at ${apiBase}`);
202
+ }
203
+ catch (err) {
204
+ console.error("Error mounting apiRoutes:", err);
205
+ }
206
+ try {
207
+ mount(routes);
208
+ }
209
+ catch (err) {
210
+ console.error("Error mounting routes:", err);
76
211
  }
77
212
  // Dynamic Router
78
- this.app.use((0, DynamicRouter_1.createDynamicRouter)(views));
79
- // 404 Fallback
213
+ this.app.use((0, DynamicRouter_1.createDynamicRouter)(resolvedViews));
214
+ // 404 and error handlers
80
215
  this.app.use((req, res) => {
81
- res.status(404).renderPage("NotFoundPage");
216
+ if (req.get("X-ArcanaJS-Request") || req.query.format === "json") {
217
+ res.status(404).json({
218
+ page: "NotFoundPage",
219
+ data: {},
220
+ params: {},
221
+ csrfToken: res.locals.csrfToken,
222
+ });
223
+ }
224
+ else {
225
+ res.status(404).renderPage("NotFoundPage");
226
+ }
82
227
  });
83
- // Error Handler
84
228
  this.app.use((err, req, res, next) => {
85
229
  console.error(err);
86
230
  const message = process.env.NODE_ENV === "production"
87
231
  ? "Internal Server Error"
88
232
  : err.message;
89
- res.status(500).renderPage("ErrorPage", { message });
233
+ if (req.get("X-ArcanaJS-Request") || req.query.format === "json") {
234
+ res.status(500).json({
235
+ page: "ErrorPage",
236
+ data: { message },
237
+ params: {},
238
+ csrfToken: res.locals.csrfToken,
239
+ });
240
+ }
241
+ else {
242
+ res.status(500).renderPage("ErrorPage", { message });
243
+ }
90
244
  });
91
245
  }
92
246
  loadViewsFromContext(context) {
@@ -157,9 +311,131 @@ class ArcanaJSServer {
157
311
  }
158
312
  start() {
159
313
  const PORT = this.config.port || process.env.PORT || 3000;
160
- this.app.listen(PORT, () => {
314
+ this.serverInstance = this.app.listen(PORT, () => {
161
315
  console.log(`Server is running on http://localhost:${PORT}`);
162
316
  });
317
+ // Optionally register process signal handlers per-instance to gracefully shutdown
318
+ const autoHandle = this.config.autoHandleSignals !== false;
319
+ if (autoHandle) {
320
+ const shutdown = async () => {
321
+ try {
322
+ await this.stop();
323
+ process.exit(0);
324
+ }
325
+ catch (err) {
326
+ console.error("Error during shutdown:", err);
327
+ process.exit(1);
328
+ }
329
+ };
330
+ this._sigintHandler = shutdown;
331
+ this._sigtermHandler = shutdown;
332
+ process.on("SIGINT", this._sigintHandler);
333
+ process.on("SIGTERM", this._sigtermHandler);
334
+ }
335
+ }
336
+ /**
337
+ * Stop the HTTP server and close DB connection if present.
338
+ */
339
+ async stop() {
340
+ // Close HTTP server
341
+ if (this.serverInstance) {
342
+ await new Promise((resolve, reject) => {
343
+ this.serverInstance.close((err) => {
344
+ if (err)
345
+ return reject(err);
346
+ resolve();
347
+ });
348
+ });
349
+ this.serverInstance = undefined;
350
+ console.log("HTTP server stopped");
351
+ }
352
+ // Close DB connection if attached to app.locals.db
353
+ const db = this.app.locals.db;
354
+ if (db) {
355
+ let closed = false;
356
+ // Try mongoose.disconnect()
357
+ try {
358
+ if (typeof db.disconnect === "function") {
359
+ await db.disconnect();
360
+ closed = true;
361
+ console.log("Database connection closed via disconnect().");
362
+ }
363
+ }
364
+ catch (err) {
365
+ console.error("Error calling disconnect() on DB client:", err);
366
+ }
367
+ // Try db.close()
368
+ if (!closed) {
369
+ try {
370
+ if (typeof db.close === "function") {
371
+ await db.close();
372
+ closed = true;
373
+ console.log("Database connection closed via close().");
374
+ }
375
+ }
376
+ catch (err) {
377
+ console.error("Error calling close() on DB client:", err);
378
+ }
379
+ }
380
+ // Try db.end()
381
+ if (!closed) {
382
+ try {
383
+ if (typeof db.end === "function") {
384
+ await db.end();
385
+ closed = true;
386
+ console.log("Database connection closed via end().");
387
+ }
388
+ }
389
+ catch (err) {
390
+ console.error("Error calling end() on DB client:", err);
391
+ }
392
+ }
393
+ // Try db.client?.close()
394
+ if (!closed) {
395
+ try {
396
+ const clientClose = db.client && db.client.close;
397
+ if (clientClose && typeof clientClose === "function") {
398
+ await db.client.close();
399
+ closed = true;
400
+ console.log("Database connection closed via db.client.close().");
401
+ }
402
+ }
403
+ catch (err) {
404
+ console.error("Error calling db.client.close() on DB client:", err);
405
+ }
406
+ }
407
+ // Try db.s?.client?.close() (internal Mongo client path)
408
+ if (!closed) {
409
+ try {
410
+ const maybeInternal = db.s && db.s.client && db.s.client.close;
411
+ if (maybeInternal && typeof maybeInternal === "function") {
412
+ await db.s.client.close();
413
+ closed = true;
414
+ console.log("Database connection closed via db.s.client.close().");
415
+ }
416
+ }
417
+ catch (err) {
418
+ console.error("Error calling db.s.client.close() on DB client:", err);
419
+ }
420
+ }
421
+ if (!closed) {
422
+ console.warn("Could not find a supported close method on the DB client; connection may remain open.");
423
+ }
424
+ }
425
+ // Remove signal handlers registered by this instance
426
+ try {
427
+ if (this._sigintHandler) {
428
+ process.removeListener("SIGINT", this._sigintHandler);
429
+ this._sigintHandler = undefined;
430
+ }
431
+ if (this._sigtermHandler) {
432
+ process.removeListener("SIGTERM", this._sigtermHandler);
433
+ this._sigtermHandler = undefined;
434
+ }
435
+ }
436
+ catch (err) {
437
+ // ignore errors while removing listeners
438
+ }
163
439
  }
164
440
  }
165
441
  exports.ArcanaJSServer = ArcanaJSServer;
@@ -86,7 +86,9 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
86
86
  setIsNavigating(true);
87
87
  try {
88
88
  const response = await fetch(newUrl, {
89
- headers: { "x-arcanajs-request": "true" },
89
+ headers: { "X-ArcanaJS-Request": "true" },
90
+ // prevent caching in dev navigation
91
+ cache: "no-store",
90
92
  });
91
93
  if (!response.ok) {
92
94
  if (response.status === 404) {
@@ -97,6 +99,15 @@ const ArcanaJSApp = ({ initialPage, initialData, initialParams = {}, initialUrl,
97
99
  }
98
100
  throw new Error(`Navigation failed: ${response.status} ${response.statusText}`);
99
101
  }
102
+ // Ensure server returned JSON. If not, fallback to full navigation reload
103
+ const contentType = response.headers.get("content-type") || "";
104
+ 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
+ window.location.href = newUrl;
109
+ return;
110
+ }
100
111
  const json = await response.json();
101
112
  // Cache the navigation result
102
113
  navigationCache.current.set(newUrl, {
@@ -8,7 +8,7 @@
8
8
  "start": "arcanajs start"
9
9
  },
10
10
  "dependencies": {
11
- "arcanajs": "^2.4.0",
11
+ "arcanajs": "^2.5.1",
12
12
  "react": "^19.2.0",
13
13
  "react-dom": "^19.2.0"
14
14
  }
@@ -43,8 +43,7 @@ body {
43
43
  }
44
44
 
45
45
  .hero-gradient {
46
- background:
47
- radial-gradient(
46
+ background: radial-gradient(
48
47
  circle at 50% 0%,
49
48
  rgba(248, 64, 2, 0.15) 0%,
50
49
  transparent 50%
@@ -95,8 +94,10 @@ body {
95
94
  }
96
95
 
97
96
  .grid-pattern {
98
- background-image:
99
- linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
97
+ background-image: linear-gradient(
98
+ rgba(255, 255, 255, 0.02) 1px,
99
+ transparent 1px
100
+ ),
100
101
  linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
101
102
  background-size: 50px 50px;
102
103
  }
@@ -0,0 +1,10 @@
1
+ export default async function dbConnect() {
2
+ // Example MongoDB connection using the official driver
3
+ const { MongoClient } = await import("mongodb");
4
+ const url = process.env.MONGO_URL || "mongodb://localhost:27017";
5
+ const dbName = process.env.MONGO_DB || "mydb";
6
+ const client = new MongoClient(url);
7
+ await client.connect();
8
+ const db = client.db(dbName);
9
+ return { client, db };
10
+ }
@@ -0,0 +1,12 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export default async function dbConnect() {
4
+ const uri =
5
+ process.env.MONGOOSE_URI ||
6
+ process.env.MONGO_URL ||
7
+ "mongodb://localhost:27017/mydb";
8
+ const options = {} as mongoose.ConnectOptions;
9
+
10
+ await mongoose.connect(uri, options);
11
+ return mongoose;
12
+ }
@@ -0,0 +1,15 @@
1
+ export default async function dbConnect() {
2
+ const mysql = await import("mysql2/promise");
3
+ const uri = process.env.MYSQL_URL;
4
+ if (uri) {
5
+ return await mysql.createConnection(uri as any);
6
+ }
7
+
8
+ const connection = await mysql.createConnection({
9
+ host: process.env.MYSQL_HOST || "localhost",
10
+ user: process.env.MYSQL_USER || "root",
11
+ database: process.env.MYSQL_DB || "mydb",
12
+ password: process.env.MYSQL_PASSWORD || undefined,
13
+ });
14
+ return connection;
15
+ }
@@ -0,0 +1,8 @@
1
+ export default async function dbConnect() {
2
+ const { Client } = await import("pg");
3
+ const connectionString =
4
+ process.env.PG_CONNECTION || process.env.DATABASE_URL;
5
+ const client = new Client({ connectionString });
6
+ await client.connect();
7
+ return client;
8
+ }
@@ -0,0 +1,37 @@
1
+ import { Request, Response } from "express";
2
+
3
+ /**
4
+ * UsersController - example controller for users endpoints.
5
+ */
6
+ export default class UsersController {
7
+ async users(req: Request, res: Response) {
8
+ try {
9
+ const normalized: any = (req as any).db || req.app?.locals?.db;
10
+ if (!normalized) return res.error("No DB connection", 500, null, null);
11
+
12
+ const client: any = normalized.client || normalized;
13
+ const db: any = normalized.db || normalized;
14
+
15
+ if (db && typeof db.collection === "function") {
16
+ const users = await db.collection("users").find().toArray();
17
+ return res.success(users, "Users retrieved successfully", 200);
18
+ }
19
+
20
+ if (client && typeof client.query === "function") {
21
+ const result = await client.query("SELECT * FROM users LIMIT 100");
22
+ const rows = result.rows || result[0] || result;
23
+ return res.success(rows, "Users retrieved successfully", 200);
24
+ }
25
+
26
+ return res.error(
27
+ "Unsupported DB client in template example",
28
+ 400,
29
+ null,
30
+ null
31
+ );
32
+ } catch (err) {
33
+ console.error(err);
34
+ return res.error("Query failed", 500, err as any, null);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,35 @@
1
+ import { ArcanaJSServer } from "arcanajs/server";
2
+ import apiRoutes from "./routes/api";
3
+ import webRoutes from "./routes/web";
4
+
5
+ // Example DB connectors included in templates:
6
+ // import mongoDb from '../db/mongo';
7
+ // import mongooseDb from '../db/mongoose';
8
+ // import pgDb from '../db/postgres';
9
+ // import mysqlDb from '../db/mysql';
10
+
11
+ const PORT = process.env.PORT || 3000;
12
+
13
+ const server = new ArcanaJSServer({
14
+ port: PORT,
15
+ routes: webRoutes,
16
+ // Separate API routes (mounted under /api by default)
17
+ apiRoutes: apiRoutes,
18
+ // To change the base path, set apiBase: '/v1' for example or similar
19
+ apiBase: "/api",
20
+ // Example: provide a dbConnect function that returns the DB client/connection.
21
+ // You can connect to MySQL/Postgres/MongoDB here and return the client.
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');
27
+ // },
28
+ // Or use one of the provided DB templates (uncomment one):
29
+ // dbConnect: mongoDb,
30
+ // dbConnect: mongooseDb,
31
+ // dbConnect: pgDb,
32
+ // dbConnect: mysqlDb,
33
+ });
34
+
35
+ server.start();
@@ -0,0 +1,6 @@
1
+ import { Route } from "arcanajs/server";
2
+ import UsersController from "../controllers/UsersController";
3
+
4
+ router.get("/users", [UsersController, "users"]);
5
+
6
+ export default Route.getRouter();
@@ -220,7 +220,7 @@ export default function HomePage() {
220
220
  strokeLinecap="round"
221
221
  strokeLinejoin="round"
222
222
  strokeWidth={2}
223
- d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2V7zm0 0V5a2 2 0 012-2h6l2 2h6a2 2 0 012 2v2H3z"
223
+ d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2V7z"
224
224
  />
225
225
  </svg>
226
226
  </div>
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.4.0",
8
+ "version": "2.5.1",
9
9
  "description": "ArcanaJS Framework",
10
10
  "main": "framework/lib/index.js",
11
11
  "types": "framework/lib/index.d.ts",
@@ -1,11 +0,0 @@
1
- import { ArcanaJSServer } from "arcanajs/server";
2
- import webRoutes from "./routes/web";
3
-
4
- const PORT = process.env.PORT || 3000;
5
-
6
- const server = new ArcanaJSServer({
7
- port: PORT,
8
- routes: webRoutes,
9
- });
10
-
11
- server.start();