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.
- package/framework/cli/templates.js +26 -11
- package/framework/lib/global.d.ts +7 -0
- package/framework/lib/server/ArcanaJSMiddleware.js +1 -1
- package/framework/lib/server/ArcanaJSServer.d.ts +36 -4
- package/framework/lib/server/ArcanaJSServer.js +319 -43
- package/framework/lib/shared/core/ArcanaJSApp.js +12 -1
- package/framework/templates/package.json +1 -1
- package/framework/templates/{globals.css → src/client/globals.css} +5 -4
- package/framework/templates/src/db/mongo.ts +10 -0
- package/framework/templates/src/db/mongoose.ts +12 -0
- package/framework/templates/src/db/mysql.ts +15 -0
- package/framework/templates/src/db/postgres.ts +8 -0
- package/framework/templates/src/server/controllers/UsersController.ts +37 -0
- package/framework/templates/src/server/index.ts +35 -0
- package/framework/templates/src/server/routes/api.ts +6 -0
- package/framework/templates/{HomePage.tsx → src/views/HomePage.tsx} +1 -1
- package/package.json +1 -1
- package/framework/templates/server-index.ts +0 -11
- /package/framework/templates/{arcanajs.png → public/arcanajs.png} +0 -0
- /package/framework/templates/{arcanajs.svg → public/arcanajs.svg} +0 -0
- /package/framework/templates/{favicon.ico → public/favicon.ico} +0 -0
- /package/framework/templates/{arcanajs.d.ts → src/arcanajs.d.ts} +0 -0
- /package/framework/templates/{client-index.tsx → src/client/index.tsx} +0 -0
- /package/framework/templates/{server-controller-home.ts → src/server/controllers/HomeController.ts} +0 -0
- /package/framework/templates/{server-routes-web.ts → src/server/routes/web.ts} +0 -0
- /package/framework/templates/{ErrorPage.tsx → src/views/ErrorPage.tsx} +0 -0
- /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
|
-
|
|
10
|
-
{ src: "
|
|
11
|
-
|
|
12
|
-
{ src: "
|
|
13
|
-
{ src: "
|
|
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
|
|
20
|
+
src: "src/server/controllers/HomeController.ts",
|
|
16
21
|
dest: "src/server/controllers/HomeController.ts",
|
|
17
22
|
},
|
|
18
23
|
{
|
|
19
|
-
src: "
|
|
20
|
-
dest: "src/
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
32
|
-
if (
|
|
33
|
-
|
|
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
|
-
//
|
|
36
|
-
if (
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
149
|
+
views: resolvedViews,
|
|
150
|
+
indexFile: path_1.default.resolve(root, indexFile),
|
|
66
151
|
layout,
|
|
67
152
|
}));
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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)(
|
|
79
|
-
// 404
|
|
213
|
+
this.app.use((0, DynamicRouter_1.createDynamicRouter)(resolvedViews));
|
|
214
|
+
// 404 and error handlers
|
|
80
215
|
this.app.use((req, res) => {
|
|
81
|
-
|
|
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
|
-
|
|
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: { "
|
|
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, {
|
|
@@ -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
|
-
|
|
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();
|
|
@@ -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-
|
|
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
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/framework/templates/{server-controller-home.ts → src/server/controllers/HomeController.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|