aaex-cli 1.1.0 → 1.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/.TODO ADDED
@@ -0,0 +1,4 @@
1
+ 1. Fix server not being able to use Suspense
2
+ - Option 1. Remake the router to always import pages directly
3
+ - Option 2. Make the server able to stream content
4
+ 2.
package/README.md CHANGED
@@ -7,13 +7,14 @@ Light weight SSR framework for react with filebased page and api routing. Builds
7
7
  - File routing using aaex-file-router (can be used seperatly)
8
8
  - API routing using hybrid solution only available in the full framework
9
9
  - SSR rendering using vites native functions + additional functionality
10
- -full typescript support (currently only typescript)
10
+ - full typescript support (currently only typescript)
11
11
  - all vite plugins that work with ssr should work
12
12
 
13
+
13
14
  ## Usage
14
15
  ```sh
15
16
  npx create-aaex-app <project-name>
16
17
  ```
17
18
 
18
- ## V1.1
19
- Added user system
19
+ ## V1.4
20
+ Added server side data loading
@@ -60,8 +60,9 @@ async function createPackageJson() {
60
60
  "npm run build:client && npm run build:server && npm run build:api",
61
61
  "build:client": "vite build --outDir dist/client",
62
62
  "build:server":
63
- "vite build --ssr .aaex/framework/server-entry.tsx --outDir dist/server",
63
+ "vite build --ssr .aaex/framework/entry-server.tsx --outDir dist/server",
64
64
  "build:api": "tsc --project tsconfig.api.json",
65
+ "build:utils": "tsc --project tsconfig.utils.json",
65
66
  preview: "cross-env NODE_ENV=production node .aaex/server/server.js",
66
67
  },
67
68
  dependencies: {
@@ -72,7 +73,11 @@ async function createPackageJson() {
72
73
  express: "^5.1.0",
73
74
  compression: "^1.8.1",
74
75
  sirv: "^3.0.2",
75
- "aaex-file-router": "^1.4.4",
76
+ "aaex-file-router": "^1.5.0",
77
+ jsonwebtoken: "^9.0.3",
78
+ mongodb: "^7.0.0",
79
+ bcrypt: "^6.0.0",
80
+ dotenv: "^17.2.3",
76
81
  },
77
82
  devDependencies: {
78
83
  typescript: "~5.9.2",
@@ -82,6 +87,8 @@ async function createPackageJson() {
82
87
  "@vitejs/plugin-react": "^5.0.2",
83
88
  vite: "^7.1.5",
84
89
  "cross-env": "^10.0.0",
90
+ "@types/bcrypt": "^6.0.0",
91
+ "@types/jsonwebtoken": "^9.0.10",
85
92
  },
86
93
  };
87
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaex-cli",
3
- "version": "1.1.0",
3
+ "version": "1.4.0",
4
4
  "description": "Command line interface for creating aaexjs app",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -12,8 +12,18 @@
12
12
  "test": "echo \"Error: no test specified\" && exit 1"
13
13
  },
14
14
  "devDependencies": {
15
+ "@types/bcrypt": "^6.0.0",
16
+ "@types/express": "^5.0.6",
17
+ "@types/jsonwebtoken": "^9.0.10",
18
+ "@types/node": "^24.10.1",
19
+ "@types/react": "^19.2.7",
20
+ "@types/react-dom": "^19.2.3",
15
21
  "@vitejs/plugin-react": "^5.1.1",
16
22
  "aaex-file-router": "^1.4.5",
23
+ "bcrypt": "^6.0.0",
24
+ "express": "^5.2.1",
25
+ "jsonwebtoken": "^9.0.3",
26
+ "mongodb": "^7.0.0",
17
27
  "react": "^19.2.1",
18
28
  "react-dom": "^19.2.1",
19
29
  "react-router": "^7.10.1",
@@ -1,53 +1,67 @@
1
1
  import { FileScanner } from "aaex-file-router/core";
2
2
  import { match } from "path-to-regexp";
3
3
 
4
- // --- Scan API folder ---
5
- const apiScanner = new FileScanner("./src/api");
4
+ // --- scan folders ---
5
+ //wrapper for the FilesScanner and output data
6
+ async function scanApiFolder(folder) {
7
+ const scanner = new FileScanner(folder);
8
+ return await scanner.get_file_data();
9
+ }
10
+
11
+ const userApiFolder = "src/api";
6
12
 
7
- const files = await apiScanner.get_file_data();
13
+ const internalApiFolder = ".aaex/api";
8
14
 
9
- const routes = BuildApiRoutes(files);
15
+ //collect file data as easily parsed arrays
16
+ const [userFiles, internalFiles] = await Promise.all([
17
+ scanApiFolder(userApiFolder),
18
+ scanApiFolder(internalApiFolder),
19
+ ]);
10
20
 
11
- export default routes;
21
+ // --- combine files ---
22
+ const fileMap = new Map();
23
+ internalFiles.forEach((f) => fileMap.set(f.relative_path, f));
24
+ userFiles.forEach((f) => fileMap.set(f.relative_path, f)); //overrides internal api with user defined routes
25
+ const combinedFiles = Array.from(fileMap.values());
12
26
 
13
- // --- Build routes from file tree ---
14
- function BuildApiRoutes(files) {
27
+ // --- build route list ---
28
+ /**Builds route object from the scanned files */
29
+ function buildApiRoutes(files) {
15
30
  const routes = [];
16
31
 
17
32
  function walk(node, currentPath) {
33
+ //recursivly iterate over child routes
18
34
  if (node.isDirectory) {
19
- node.children?.forEach((child) => {
20
- walk(child, currentPath + "/" + node.name);
21
- });
35
+ node.children?.forEach((c) => walk(c, currentPath + "/" + node.name));
22
36
  return;
23
37
  }
24
-
25
38
  const filePath = currentPath + "/" + node.name;
26
-
39
+ //build route path from filePath
27
40
  let route = filePath
28
- .replace(/^.*src\/api/, "/api") // replace root
29
- .replace(/\.ts$/, "") // remove extension
30
- .replace(/\[(.+?)\]/g, ":$1") // dynamic params
31
- .replace("/index", ""); // remove index from end
32
-
33
- routes.push({
34
- route,
35
- filePath: node.relative_path,
36
- });
41
+ .replace(/^.*(src\/api|api)/, "/api") //removed parent folder like src
42
+ .replace(/\.ts|js$/, "") //removes file extension
43
+ .replace(/\[(.+?)\]/g, ":$1") //converts [slug] to :slug
44
+ .replace("/index", "");
45
+
46
+ routes.push({ route, filePath: node.relative_path });
37
47
  }
38
48
 
39
- files.forEach((f) => walk(f, ""));
49
+ files.forEach((file) => walk(file, ""));
40
50
  return routes;
41
51
  }
42
52
 
43
- // --- Match path to route ---
44
- export function PathToRoute(path, routes) {
53
+ const routes = buildApiRoutes(combinedFiles);
54
+
55
+ // --- match path to route ---
56
+ /** Matches the given path to API routes generated from the api folder + the aaex/api folder*/
57
+ function pathToRoute(pathname) {
45
58
  for (const r of routes) {
46
59
  const matcher = match(r.route, { decode: decodeURIComponent });
47
- const matched = matcher(path); // pass full path including /api
48
- if (matched) {
49
- return { route: r, params: matched.params }; // return params for dynamic segments
50
- }
60
+ const matched = matcher(pathname);
61
+ if (matched) return { route: r, params: matched.params };
51
62
  }
52
63
  return null;
53
64
  }
65
+
66
+ // --- export ---
67
+ export { routes, pathToRoute };
@@ -0,0 +1,57 @@
1
+ import { connectToDatabase } from "../../framework/database/mongodb";
2
+ import type { Request, Response } from "express";
3
+ import bcrypt from "bcrypt";
4
+ import jwt from "jsonwebtoken";
5
+ import { LoginUser } from "../../../src/models/User";
6
+
7
+ export const POST = async (req: Request, res: Response) => {
8
+ const { email, password }: LoginUser = req.body;
9
+
10
+ if (!email || !password) {
11
+ return res.status(400).json({ error: "Missing fields!" });
12
+ }
13
+
14
+ const { db } = await connectToDatabase();
15
+
16
+ if (!process.env.JWT_SECRET) {
17
+ console.error("Missing: JWT_SECRET from environment variables");
18
+ return res
19
+ .status(500)
20
+ .json({ error: "Internal server error! Try again later" });
21
+ }
22
+
23
+ const normalizedEmail = email.trim().toLowerCase();
24
+ const user = await db.collection("users").findOne({ email: normalizedEmail });
25
+
26
+ if (!user) {
27
+ return res.status(400).json({ error: "Invalid email or password" });
28
+ }
29
+
30
+ const compared = await bcrypt.compare(password, user.password);
31
+
32
+ if (!compared) {
33
+ return res.status(400).json({ error: "Invalid email or password" });
34
+ }
35
+
36
+ const expiration = process.env.JWT_EXP ?? "24h";
37
+
38
+ const token = jwt.sign(
39
+ {
40
+ id: user._id.toString(),
41
+ email: user.email,
42
+ username: user.username,
43
+ },
44
+ process.env.JWT_SECRET as string,
45
+ { expiresIn: expiration as any } //fixes stupid thing where it wont accept the sring variable because its not number | ms.stringvlue | undefined
46
+ );
47
+
48
+ return res.status(200).json({
49
+ ok: true,
50
+ user: {
51
+ id: user._id.toString(),
52
+ name: user.username,
53
+ email: user.email,
54
+ },
55
+ token,
56
+ });
57
+ };
@@ -0,0 +1,34 @@
1
+ import { Request, Response } from "express";
2
+ import bcrypt from "bcrypt";
3
+ import { connectToDatabase } from "../../framework/database/mongodb";
4
+ import { CreateUser } from "../../../src/models/User";
5
+ export const POST = async (req: Request, res: Response) => {
6
+
7
+ const { email, username, password, confirmPass }: CreateUser = req.body;
8
+
9
+ if (!username || !email || !password || !confirmPass)
10
+ return res.status(400).json({ error: "Missing fields" });
11
+
12
+ if (password !== confirmPass)
13
+ return res.status(400).json({ error: "Passwords do not match" });
14
+
15
+ const { db } = await connectToDatabase();
16
+
17
+ const exists = await db.collection("users").findOne({ email });
18
+ if (exists)
19
+ return res
20
+ .status(409)
21
+ .json({ error: "User with that email already exists" });
22
+
23
+ const salt = 10;
24
+ const hashed = await bcrypt.hash(password, salt);
25
+
26
+ await db.collection("users").insertOne({
27
+ username,
28
+ email,
29
+ password: hashed,
30
+ createdAt: new Date(),
31
+ });
32
+
33
+ return res.status(201).json({ ok: true });
34
+ };
@@ -0,0 +1,18 @@
1
+ import jwt from "jsonwebtoken";
2
+ import type { Request, Response } from "express";
3
+
4
+ export async function POST(req: Request, res: Response) {
5
+ const { token } = req.body;
6
+
7
+ if (!process.env.JWT_SECRET) {
8
+ console.error("Missing: JWT_SECRET from environment");
9
+ return res.status(500).json({error: "Internal server error!"});
10
+ }
11
+
12
+ try {
13
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
14
+ return res.status(200).json({ valid: true, user: decoded });
15
+ } catch (err) {
16
+ return res.status(401).json({ valid: false });
17
+ }
18
+ }
@@ -0,0 +1,29 @@
1
+ import { MongoClient, Db } from "mongodb";
2
+
3
+ const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
4
+ const DB_NAME = process.env.DB_NAME || "mydatabase";
5
+
6
+ if (!MONGO_URI) {
7
+ throw new Error("Please define the MONGO_URI environment variable inside .env");
8
+ }
9
+
10
+ let cachedClient: MongoClient | null = null;
11
+ let cachedDb: Db | null = null;
12
+
13
+ export async function connectToDatabase(): Promise<{ client: MongoClient; db: Db }> {
14
+ // Return cached connection if it exists
15
+ if (cachedClient && cachedDb) {
16
+ return { client: cachedClient, db: cachedDb };
17
+ }
18
+
19
+ const client = new MongoClient(MONGO_URI);
20
+ await client.connect();
21
+ const db = client.db(DB_NAME);
22
+
23
+ cachedClient = client;
24
+ cachedDb = db;
25
+
26
+ console.log("MongoDB connected:", DB_NAME);
27
+
28
+ return { client, db };
29
+ }
@@ -1,16 +1,16 @@
1
- import "../../src/index.css"
2
- import { StrictMode, Suspense } from "react";
1
+ import "../../src/index.css";
2
+ import { StrictMode } from "react";
3
3
  import { hydrateRoot } from "react-dom/client";
4
- import App from "../../src/App"
4
+ import App from "../../src/App";
5
5
  import { BrowserRouter } from "react-router";
6
6
 
7
+ const initialData = (window as any).__INITIAL_DATA__;
8
+
7
9
  hydrateRoot(
8
10
  document.getElementById("root") as HTMLElement,
9
11
  <StrictMode>
10
12
  <BrowserRouter>
11
- <Suspense>
12
- <App />
13
- </Suspense>
13
+ <App initialData={initialData} />
14
14
  </BrowserRouter>
15
15
  </StrictMode>
16
16
  );
@@ -3,17 +3,14 @@ import { renderToString } from "react-dom/server";
3
3
  import { StaticRouter } from "react-router";
4
4
  import App from "../../src/App";
5
5
 
6
- export function render(_url: string) {
7
- const url = `/${_url}`;
6
+ export function render(_url: string, initialData= {}) {
7
+ const url = `${_url}`;
8
8
 
9
- // call your SSR function or API here and pass the result as props
10
9
 
11
10
  const html = renderToString(
12
11
  <StrictMode>
13
12
  <StaticRouter location={url}>
14
- <Suspense>
15
- <App />
16
- </Suspense>
13
+ <App initialData={initialData} />
17
14
  </StaticRouter>
18
15
  </StrictMode>
19
16
  );
@@ -0,0 +1,60 @@
1
+ export default function routeMatcher(routes, url) {
2
+ const segments = url.split("/").filter(Boolean); // "test/hej" → ["test", "hej"]
3
+
4
+ return matchLevel(routes, segments);
5
+ }
6
+
7
+ function matchLevel(routes, segments) {
8
+ for (const route of routes) {
9
+ const result = matchRoute(route, segments);
10
+
11
+ if (result) return result;
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ function matchRoute(route, segments) {
18
+ const isParam = /^:[a-zA-Z0-9_]+$/;
19
+
20
+ const [current, ...rest] = segments;
21
+
22
+ // Root index route
23
+ if (route.path === "" && segments.length === 0) {
24
+ return { route, params: {} };
25
+ }
26
+
27
+ // Static match
28
+ if (route.path === current) {
29
+ if (rest.length === 0) {
30
+ return { route, params: {} };
31
+ }
32
+
33
+ // Has children → go deeper
34
+ if (route.children) {
35
+ return matchLevel(route.children, rest);
36
+ }
37
+ }
38
+
39
+ // Dynamic match → :slug , :id osv
40
+ if (isParam.test(route.path)) {
41
+ const paramName = route.path.slice(1);
42
+
43
+ if (rest.length === 0) {
44
+ return { route, params: { [paramName]: current } };
45
+ }
46
+
47
+ // Has children → go deeper
48
+ if (route.children) {
49
+ const matchedChild = matchLevel(route.children, rest);
50
+ if (matchedChild) {
51
+ return {
52
+ route: matchedChild.route,
53
+ params: { [paramName]: current, ...matchedChild.params },
54
+ };
55
+ }
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
@@ -2,15 +2,18 @@ import fs from "node:fs/promises";
2
2
  import express from "express";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import path from "node:path";
5
+ import dotenv from "dotenv";
6
+ dotenv.config();
5
7
 
6
8
  // server.js is now in .aaex/server/
7
- const frameworkRoot = path.resolve(".aaex"); // absolute path to .aaex
8
9
  const projectRoot = path.resolve("."); // root of the project
9
-
10
+ let serverRoutes;
10
11
  // Import BuildApiRoutes
11
- const { default: apiRoutes, PathToRoute } = await import(
12
- pathToFileURL(`${frameworkRoot}/BuildApiRoutes.js`).href
13
- );
12
+ import * as BuildApiRoutes from "../BuildApiRoutes.js";
13
+ import routeMatcher from "../matchServerRoutes.js";
14
+
15
+ const apiRoutes = BuildApiRoutes.default; // default export
16
+ const PathToRoute = BuildApiRoutes.pathToRoute; // named export
14
17
 
15
18
  // Constants
16
19
  const isProduction = process.env.NODE_ENV === "production";
@@ -35,6 +38,7 @@ if (!isProduction) {
35
38
  root: projectRoot,
36
39
  base,
37
40
  });
41
+ serverRoutes = (await vite.ssrLoadModule("/src/server-routes.ts")).default;
38
42
  app.use(vite.middlewares);
39
43
  } else {
40
44
  const compression = (await import("compression")).default;
@@ -44,16 +48,24 @@ if (!isProduction) {
44
48
  base,
45
49
  sirv(path.join(projectRoot, "dist/client"), { extensions: [] })
46
50
  );
51
+
52
+ const serverRoutesModule = await import(
53
+ pathToFileURL(path.join(projectRoot, "dist/src/server-routes.js")).href
54
+ );
55
+
56
+ serverRoutes = serverRoutesModule.default;
47
57
  }
48
58
 
59
+ // parse JSON bodies
60
+ app.use(express.json());
61
+
62
+ // parse URL-encoded bodies (optional, for form POSTs)
63
+ app.use(express.urlencoded({ extended: true }));
64
+
49
65
  // API routing
50
66
  app.use("/api", async (req, res) => {
51
- console.log(req.path);
52
-
53
67
  const routeMatch = PathToRoute(req.path, apiRoutes);
54
68
 
55
- console.log(routeMatch);
56
-
57
69
  if (!routeMatch)
58
70
  return res.status(404).json({ error: "API route not found" });
59
71
 
@@ -62,7 +74,11 @@ app.use("/api", async (req, res) => {
62
74
 
63
75
  if (!isProduction) {
64
76
  // DEV: Vite handles TS/JS loading
65
- modulePath = `/src/api${route.filePath.replace(/^src\/api/, "")}`;
77
+ if (route.filePath.split("/")[0] == ".aaex") {
78
+ modulePath = `${route.filePath.replace(/^src\/api/, "")}`;
79
+ } else {
80
+ modulePath = `/src/api${route.filePath.replace(/^src\/api/, "")}`;
81
+ }
66
82
  } else {
67
83
  // PROD: bundled JS
68
84
  modulePath = pathToFileURL(
@@ -93,9 +109,24 @@ app.use("/api", async (req, res) => {
93
109
  });
94
110
 
95
111
  // SSR HTML
96
- app.use("*all", async (req, res) => {
112
+ app.use(/.*/, async (req, res) => {
97
113
  try {
98
- const url = req.originalUrl.replace(base, "");
114
+ let url = req.originalUrl;
115
+
116
+ const routeMatch = routeMatcher(serverRoutes, url);
117
+
118
+ if (!routeMatch) {
119
+ return res.status(404).send("Not found");
120
+ }
121
+
122
+ // Dynamicly import the file
123
+ const mod = await vite.ssrLoadModule(routeMatch.route.modulePath);
124
+
125
+ // Call load if it exist
126
+ let initialData = {};
127
+ if (mod.load) {
128
+ initialData = await mod.load(routeMatch.params);
129
+ }
99
130
 
100
131
  let template;
101
132
  let render;
@@ -114,11 +145,25 @@ app.use("*all", async (req, res) => {
114
145
  ).render;
115
146
  }
116
147
 
117
- const rendered = await render(url);
148
+ const rendered = await render(url, initialData);
149
+
150
+ function XSSPrevention(unsafeString) {
151
+ return unsafeString.replace(/</g, "//u003c");
152
+ // .replace(/>/g, "&gt")
153
+ // .replace(/'/g, "&#39")
154
+ // .replace(/"/g, "&#34");
155
+ }
156
+
157
+ const serializedData = JSON.stringify(initialData);
158
+ const safeData = XSSPrevention(serializedData);
118
159
 
119
160
  const html = template
120
161
  .replace("<!--app-head-->", rendered.head ?? "")
121
- .replace("<!--app-html-->", rendered.html ?? "");
162
+ .replace("<!--app-html-->", rendered.html ?? "")
163
+ .replace(
164
+ "<!--initial-data-->",
165
+ `<script>window.__INITIAL_DATA__ = ${safeData}</script>`
166
+ );
122
167
 
123
168
  res.status(200).set({ "Content-Type": "text/html" }).send(html);
124
169
  } catch (e) {
@@ -0,0 +1,51 @@
1
+ //credit to https://github.com/vitejs/vite/issues/16515
2
+
3
+ import type {Plugin, ViteDevServer} from 'vite';
4
+
5
+ const virtualCssPath = '/@virtual:ssr-css.css';
6
+
7
+ const collectedStyles = new Map<string, string>();
8
+
9
+ export function pluginSsrDevFoucFix(): Plugin {
10
+ let server: ViteDevServer;
11
+
12
+ return {
13
+ name: 'ssr-dev-FOUC-fix',
14
+ apply: 'serve',
15
+ transform(code: string, id: string) {
16
+ if (id.includes('node_modules')) return null;
17
+ if (id.includes('.css')) {
18
+ collectedStyles.set(id, code);
19
+ }
20
+ return null;
21
+ },
22
+ configureServer(server_) {
23
+ server = server_;
24
+
25
+ server.middlewares.use((req, _res, next) => {
26
+ if (req.url === virtualCssPath) {
27
+ _res.setHeader('Content-Type', 'text/css');
28
+ _res.write(Array.from(collectedStyles.values()).join('\n'));
29
+ _res.end();
30
+ return;
31
+ }
32
+ next();
33
+ });
34
+ },
35
+
36
+ transformIndexHtml: {
37
+ handler: async () => {
38
+ return [
39
+ {
40
+ tag: 'link',
41
+ injectTo: 'head',
42
+ attrs: {
43
+ rel: 'stylesheet',
44
+ href: virtualCssPath,
45
+ },
46
+ },
47
+ ];
48
+ },
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,63 @@
1
+ export function setCookie(
2
+ name: string,
3
+ value: string,
4
+ days = 7,
5
+ options: {
6
+ path?: string;
7
+ secure?: boolean;
8
+ sameSite?: "Strict" | "Lax" | "None";
9
+ domain?: string;
10
+ } = {}
11
+ ) {
12
+ const maxAge = days * 24 * 60 * 60;
13
+ const encoded = encodeURIComponent(value);
14
+
15
+ let cookie = `${name}=${encoded}; max-age=${maxAge}; path=${
16
+ options.path ?? "/"
17
+ }`;
18
+
19
+ if (options.secure) cookie += "; secure";
20
+ if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
21
+ if (options.domain) cookie += `; domain=${options.domain}`;
22
+
23
+ document.cookie = cookie;
24
+ }
25
+ export function getCookie(name: string) {
26
+ const cookies = document.cookie.split("; ");
27
+
28
+ for (const c of cookies) {
29
+ const [key, ...rest] = c.split("=");
30
+ if (key === name) {
31
+ return decodeURIComponent(rest.join("="));
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ function cookieToJSON(cookie: string) {
39
+ const parts = cookie.split("; ");
40
+ const result: Record<string, string> = {};
41
+
42
+ for (const part of parts) {
43
+ const [key, ...rest] = part.split("=");
44
+ result[key] = rest.join("=");
45
+ }
46
+
47
+ return result;
48
+ }
49
+
50
+ export function getCookieAsJSON(name: string) {
51
+ const raw = getCookie(name);
52
+ if (!raw) return null;
53
+ return cookieToJSON(raw);
54
+ }
55
+
56
+ export function deleteCookie(
57
+ name: string,
58
+ options: { path?: string; domain?: string } = {}
59
+ ) {
60
+ let cookie = `${name}=; max-age=0; path=${options.path ?? "/"}`;
61
+ if (options.domain) cookie += `; domain=${options.domain}`;
62
+ document.cookie = cookie;
63
+ }
package/template/.env ADDED
@@ -0,0 +1,5 @@
1
+ # edit to fit your needs
2
+ MONGO_URI="mongodb://localhost:27018/"
3
+ DB_NAME="mydatabase"
4
+ JWT_SECRET="your-jwt-secret"
5
+ JWT_EXP="24h"
@@ -1,9 +1,10 @@
1
- <!doctype html>
1
+ <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="stylesheet" href="./src/index.css" />
7
8
  <title>AaExJS</title>
8
9
  <!--app-head-->
9
10
  </head>
@@ -1,23 +1,34 @@
1
1
  import "./App.css";
2
- import { Route, RouteObject, Routes } from "react-router";
3
- import routes from "./routes";
2
+ import { Route, Routes } from "react-router";
3
+ import serverRoutes from "./server-routes";
4
+ import { createElement } from "react";
4
5
 
5
- function renderRoutes(routesArray: RouteObject[]) {
6
+ function renderRoutes(routesArray: any[], initialData: any) {
6
7
  return routesArray.map((route) => {
7
- if (route.children && route.children.length > 0) {
8
+ const element = createElement(route.element as any, { ...initialData });
9
+
10
+ if (route.children?.length) {
8
11
  return (
9
- <Route path={route.path} element={route.element}>
10
- {renderRoutes(route.children)}
12
+ <Route path={route.path} element={element}>
13
+ {renderRoutes(route.children, initialData)}
11
14
  </Route>
12
15
  );
13
- } else {
14
- return <Route path={route.path} element={route.element} />;
15
16
  }
17
+
18
+ return <Route path={route.path} element={element} />;
16
19
  });
17
20
  }
18
21
 
19
- function App() {
20
- return <Routes>{renderRoutes(routes)}</Routes>;
22
+ interface AppProps {
23
+ initialData?: any;
24
+ }
25
+
26
+ function App({ initialData }: AppProps) {
27
+ return (
28
+ <>
29
+ <Routes>{renderRoutes(serverRoutes, initialData)}</Routes>;
30
+ </>
31
+ );
21
32
  }
22
33
 
23
34
  export default App;
@@ -0,0 +1,33 @@
1
+ //* AUTO GENERATED: DO NOT EDIT
2
+ import React from 'react';
3
+ import Index from './pages/index.tsx';
4
+ import Login from './pages/login.tsx';
5
+ import Register from './pages/register.tsx';
6
+ import Slug from './pages/test/[slug].tsx';
7
+ import type { RouteObject } from 'react-router-dom';
8
+
9
+ const routes: RouteObject[] = [
10
+ {
11
+ "path": "",
12
+ "element": React.createElement(Index)
13
+ },
14
+ {
15
+ "path": "login",
16
+ "element": React.createElement(Login)
17
+ },
18
+ {
19
+ "path": "register",
20
+ "element": React.createElement(Register)
21
+ },
22
+ {
23
+ "path": "test",
24
+ "children": [
25
+ {
26
+ "path": ":slug",
27
+ "element": React.createElement(Slug)
28
+ }
29
+ ]
30
+ }
31
+ ];
32
+
33
+ export default routes;
@@ -0,0 +1,48 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getCookie } from "../../.aaex/utils/cookies";
3
+ import { CookieUser } from "../models/User";
4
+
5
+ export function useAuth() {
6
+ const [user, setUser] = useState<CookieUser>({
7
+ username: "",
8
+ email: "",
9
+ id: "",
10
+ });
11
+ const [loading, setLoading] = useState(true);
12
+ const [valid, setValid] = useState(false);
13
+
14
+ useEffect(() => {
15
+ async function checkAuth() {
16
+ const token = getCookie("token");
17
+ if (!token) {
18
+ setLoading(false);
19
+ return;
20
+ }
21
+
22
+ try {
23
+ const res = await fetch("/api/auth/validate", {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({ token }),
27
+ });
28
+
29
+ const data = await res.json();
30
+
31
+ if (data.valid) {
32
+ setValid(true);
33
+ setUser(data.user);
34
+ } else {
35
+ setValid(false);
36
+ }
37
+ } catch (err) {
38
+ setValid(false);
39
+ }
40
+
41
+ setLoading(false);
42
+ }
43
+
44
+ checkAuth();
45
+ }, []);
46
+
47
+ return { user, valid, loading };
48
+ }
@@ -14,6 +14,13 @@
14
14
  -webkit-text-size-adjust: 100%;
15
15
  }
16
16
 
17
+ #root {
18
+ /* centeres all content along the x-axis */
19
+ width: 100%;
20
+ display: flex;
21
+ justify-content: center;
22
+ }
23
+
17
24
  a {
18
25
  font-weight: 500;
19
26
  color: #646cff;
@@ -0,0 +1,13 @@
1
+ export type User = {
2
+ id: string;
3
+ email: string;
4
+ username: string;
5
+ password: string;
6
+ confirmPass: string;
7
+ };
8
+
9
+ export type CreateUser = Omit<User, "id">; //should not be edited
10
+
11
+ export type LoginUser = Pick<User, "email" | "password">; //should not be edited
12
+
13
+ export type CookieUser = Omit<User, "password" | "confirmPass">;
@@ -1,3 +1,50 @@
1
- export default function Home() {
2
- return <>Hello AaEx</>;
1
+ import { useAuth } from "../hooks/useAuth.js";
2
+ import { FileLink } from "aaex-file-router";
3
+ import { FileRoutes } from "../routeTypes.js";
4
+
5
+ /** ------- Ideally same type as the API imported from /types or /interfaces ------- */
6
+ type PageData = {
7
+ hello: string;
8
+ };
9
+
10
+ type PageError = {
11
+ error: string;
12
+ };
13
+
14
+ /** -------- */
15
+ export async function load(): Promise<PageData | PageError> {
16
+ const res = await fetch("http://localhost:5173/api/helloworld");
17
+
18
+ if (!res.ok) {
19
+ return { error: "internal server error" };
20
+ }
21
+
22
+ const data = await res.json();
23
+
24
+ if (data) {
25
+ return data;
26
+ }
27
+
28
+ return { error: "Unknown error" };
29
+ }
30
+
31
+ export default function Home({ hello }: PageData) {
32
+ const { user, valid, loading } = useAuth();
33
+
34
+ if (loading) return <p>Checking authentication...</p>;
35
+ if (!valid)
36
+ return (
37
+ <p>
38
+ You are not logged in! <br />
39
+ <FileLink<FileRoutes> to="/login">Login</FileLink>
40
+ </p>
41
+ );
42
+
43
+ return (
44
+ <>
45
+ <p>Welcome, {user ? user.username : ""} </p>
46
+ <br />
47
+ <p>Server rendered content: {hello}</p>
48
+ </>
49
+ );
3
50
  }
@@ -0,0 +1,144 @@
1
+ import { useState } from "react";
2
+ import { setCookie } from "../../.aaex/utils/cookies";
3
+ import { LoginUser } from "../models/User";
4
+
5
+ export default function Login() {
6
+ const [fields, setFields] = useState<LoginUser>({
7
+ email: "",
8
+ password: "",
9
+ });
10
+
11
+ const [loading, setLoading] = useState(false);
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [success, setSuccess] = useState<string | null>(null);
14
+
15
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ const { name, value } = e.target;
17
+ setFields((prev) => ({ ...prev, [name]: value }));
18
+ };
19
+
20
+ const handleSubmit = async (e: React.FormEvent) => {
21
+ e.preventDefault();
22
+ setError(null);
23
+ setSuccess(null);
24
+ setLoading(true);
25
+
26
+ try {
27
+ const res = await fetch("/api/auth/login", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(fields),
31
+ });
32
+
33
+ const data = await res.json();
34
+
35
+ if (!res.ok) {
36
+ setError(data.error || "Invalid credentials");
37
+ } else {
38
+ setSuccess("Logged in!");
39
+
40
+ // Store token + user in cookies
41
+ setCookie("token", data.token);
42
+ setCookie("user", JSON.stringify(data.user));
43
+
44
+ // Optional: redirect
45
+ window.location.href = "/";
46
+ }
47
+ } catch (err) {
48
+ setError("Server error");
49
+ }
50
+
51
+ setLoading(false);
52
+ };
53
+
54
+ return (
55
+ <div style={styles.page}>
56
+ <form onSubmit={handleSubmit} style={styles.card}>
57
+ <h2 style={styles.title}>Login</h2>
58
+
59
+ <input
60
+ name="email"
61
+ type="email"
62
+ placeholder="Email"
63
+ value={fields.email}
64
+ onChange={handleChange}
65
+ required
66
+ style={styles.input}
67
+ />
68
+
69
+ <input
70
+ name="password"
71
+ type="password"
72
+ placeholder="Password"
73
+ value={fields.password}
74
+ onChange={handleChange}
75
+ required
76
+ style={styles.input}
77
+ />
78
+
79
+ <button type="submit" disabled={loading} style={styles.button}>
80
+ {loading ? "Logging in..." : "Login"}
81
+ </button>
82
+
83
+ {error && <div style={styles.error}>{error}</div>}
84
+ {success && <div style={styles.success}>{success}</div>}
85
+ </form>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ const styles: Record<string, React.CSSProperties> = {
91
+ page: {
92
+ minHeight: "100vh",
93
+ display: "flex",
94
+ alignItems: "center",
95
+ justifyContent: "center",
96
+ padding: "20px",
97
+ },
98
+ card: {
99
+ width: "100%",
100
+ maxWidth: "420px",
101
+ padding: "30px",
102
+ borderRadius: "12px",
103
+ boxShadow: "0 8px 20px rgba(0,0,0,0.06)",
104
+ display: "flex",
105
+ flexDirection: "column",
106
+ gap: "15px",
107
+ },
108
+ title: {
109
+ textAlign: "center",
110
+ marginBottom: "5px",
111
+ fontSize: "1.5rem",
112
+ },
113
+ input: {
114
+ padding: "12px 15px",
115
+ borderRadius: "8px",
116
+ border: "1px solid #ddd",
117
+ fontSize: "1rem",
118
+ outline: "none",
119
+ transition: "all 0.2s",
120
+ },
121
+ button: {
122
+ marginTop: "10px",
123
+ padding: "12px",
124
+ background: "#4f46e5",
125
+ color: "white",
126
+ border: "none",
127
+ fontSize: "1rem",
128
+ borderRadius: "8px",
129
+ cursor: "pointer",
130
+ transition: "0.2s",
131
+ },
132
+ error: {
133
+ marginTop: "10px",
134
+ color: "red",
135
+ textAlign: "center",
136
+ fontSize: "0.9rem",
137
+ },
138
+ success: {
139
+ marginTop: "10px",
140
+ color: "green",
141
+ textAlign: "center",
142
+ fontSize: "0.9rem",
143
+ },
144
+ };
@@ -0,0 +1,168 @@
1
+ import { useState } from "react";
2
+ import { CreateUser } from "../models/User";
3
+
4
+ export default function Register() {
5
+ const [fields, setFields] = useState<CreateUser>({
6
+ email: "",
7
+ password: "",
8
+ confirmPass: "",
9
+ username: "",
10
+ });
11
+
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState<string | null>(null);
14
+ const [success, setSuccess] = useState<string | null>(null);
15
+
16
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17
+ const { name, value } = e.target;
18
+
19
+ setFields((prev) => ({
20
+ ...prev,
21
+ [name]: value,
22
+ }));
23
+ };
24
+
25
+ const handleSubmit = async (e: React.FormEvent) => {
26
+ e.preventDefault();
27
+ setError(null);
28
+ setSuccess(null);
29
+ setLoading(true);
30
+
31
+ if (fields.password !== fields.confirmPass) {
32
+ setError("Passwords do not match.");
33
+ setLoading(false);
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const res = await fetch("/api/auth/register", {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(fields),
42
+ });
43
+
44
+ const data = await res.json();
45
+
46
+ if (!res.ok) {
47
+ setError(data.error || "Unknown error");
48
+ } else {
49
+ setSuccess("Account created successfully!");
50
+ }
51
+ } catch (err) {
52
+ setError("Server error");
53
+ }
54
+
55
+ setLoading(false);
56
+ };
57
+
58
+ return (
59
+ <div style={styles.page}>
60
+ <form onSubmit={handleSubmit} style={styles.card}>
61
+ <h2 style={styles.title}>Register</h2>
62
+
63
+ <input
64
+ name="email"
65
+ type="email"
66
+ placeholder="Email"
67
+ value={fields.email}
68
+ onChange={handleChange}
69
+ required
70
+ style={styles.input}
71
+ />
72
+
73
+ <input
74
+ name="username"
75
+ type="text"
76
+ placeholder="Username"
77
+ value={fields.username}
78
+ onChange={handleChange}
79
+ required
80
+ style={styles.input}
81
+ />
82
+
83
+ <input
84
+ name="password"
85
+ type="password"
86
+ placeholder="Password"
87
+ value={fields.password}
88
+ onChange={handleChange}
89
+ required
90
+ style={styles.input}
91
+ />
92
+
93
+ <input
94
+ name="confirmPass"
95
+ type="password"
96
+ placeholder="Confirm Password"
97
+ value={fields.confirmPass}
98
+ onChange={handleChange}
99
+ required
100
+ style={styles.input}
101
+ />
102
+
103
+ <button type="submit" disabled={loading} style={styles.button}>
104
+ {loading ? "Registering..." : "Register"}
105
+ </button>
106
+
107
+ {error && <div style={styles.error}>{error}</div>}
108
+ {success && <div style={styles.success}>{success}</div>}
109
+ </form>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ const styles: Record<string, React.CSSProperties> = {
115
+ page: {
116
+ minHeight: "100vh",
117
+ display: "flex",
118
+ alignItems: "center",
119
+ justifyContent: "center",
120
+ padding: "20px",
121
+ },
122
+ card: {
123
+ width: "100%",
124
+ maxWidth: "420px",
125
+ padding: "30px",
126
+ borderRadius: "12px",
127
+ boxShadow: "0 8px 20px rgba(0,0,0,0.06)",
128
+ display: "flex",
129
+ flexDirection: "column",
130
+ gap: "15px",
131
+ },
132
+ title: {
133
+ textAlign: "center",
134
+ marginBottom: "5px",
135
+ fontSize: "1.5rem",
136
+ },
137
+ input: {
138
+ padding: "12px 15px",
139
+ borderRadius: "8px",
140
+ border: "1px solid #ddd",
141
+ fontSize: "1rem",
142
+ outline: "none",
143
+ transition: "all 0.2s",
144
+ },
145
+ button: {
146
+ marginTop: "10px",
147
+ padding: "12px",
148
+ background: "#4f46e5",
149
+ color: "white",
150
+ border: "none",
151
+ fontSize: "1rem",
152
+ borderRadius: "8px",
153
+ cursor: "pointer",
154
+ transition: "0.2s",
155
+ },
156
+ error: {
157
+ marginTop: "10px",
158
+ color: "red",
159
+ textAlign: "center",
160
+ fontSize: "0.9rem",
161
+ },
162
+ success: {
163
+ marginTop: "10px",
164
+ color: "green",
165
+ textAlign: "center",
166
+ fontSize: "0.9rem",
167
+ },
168
+ };
@@ -0,0 +1,7 @@
1
+ import { useParams } from "react-router";
2
+
3
+ export default function Test() {
4
+ const { slug } = useParams();
5
+
6
+ return <>{slug}</>;
7
+ }
@@ -0,0 +1,7 @@
1
+ // AUTO-GENERATED: DO NOT EDIT
2
+ export type FileRoutes =
3
+ | "/"
4
+ | "/login"
5
+ | "/register"
6
+ | "/test"
7
+ | "/test/{string}";
@@ -0,0 +1,37 @@
1
+ //* AUTO GENERATED: DO NOT EDIT
2
+ import React from 'react';
3
+ import Index from './pages/index.tsx';
4
+ import Login from './pages/login.tsx';
5
+ import Register from './pages/register.tsx';
6
+ import Slug from './pages/test/[slug].tsx';
7
+
8
+
9
+ const serverRoutes: any[] = [
10
+ {
11
+ "path": "",
12
+ "element": Index,
13
+ "modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/index.tsx"
14
+ },
15
+ {
16
+ "path": "login",
17
+ "element": Login,
18
+ "modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/login.tsx"
19
+ },
20
+ {
21
+ "path": "register",
22
+ "element": Register,
23
+ "modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/register.tsx"
24
+ },
25
+ {
26
+ "path": "test",
27
+ "children": [
28
+ {
29
+ "path": ":slug",
30
+ "element": Slug,
31
+ "modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/test/[slug].tsx"
32
+ }
33
+ ]
34
+ }
35
+ ];
36
+
37
+ export default serverRoutes;
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2024",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "jsx": "react-jsx",
10
+ "esModuleInterop": true
11
+ },
12
+ "include": [
13
+ ".aaex/api",
14
+ "src/api",
15
+ ".aaex/utils",
16
+ "src/server-routes.ts",
17
+ "src/hooks"
18
+ ],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -1,8 +1,9 @@
1
- import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react'
3
- import { aaexFileRouter } from 'aaex-file-router/plugin'
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { aaexServerRouter } from "aaex-file-router/plugin";
4
+ import { pluginSsrDevFoucFix } from "./.aaex/utils/ServerLoadCssImports";
4
5
 
5
6
  // https://vite.dev/config/
6
7
  export default defineConfig({
7
- plugins: [react(),aaexFileRouter()],
8
- })
8
+ plugins: [react(), aaexServerRouter(), pluginSsrDevFoucFix()],
9
+ });
@@ -1,13 +0,0 @@
1
- //* AUTO GENERATED: DO NOT EDIT
2
- import React from 'react';
3
- import Index from './pages/index.tsx';
4
- import type { RouteObject } from 'react-router-dom';
5
-
6
- const routes: RouteObject[] = [
7
- {
8
- "path": "",
9
- "element": React.createElement(Index)
10
- }
11
- ];
12
-
13
- export default routes;