aaex-cli 1.2.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
@@ -73,7 +73,7 @@ async function createPackageJson() {
73
73
  express: "^5.1.0",
74
74
  compression: "^1.8.1",
75
75
  sirv: "^3.0.2",
76
- "aaex-file-router": "^1.4.4",
76
+ "aaex-file-router": "^1.5.0",
77
77
  jsonwebtoken: "^9.0.3",
78
78
  mongodb: "^7.0.0",
79
79
  bcrypt: "^6.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaex-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Command line interface for creating aaexjs app",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -16,6 +16,8 @@
16
16
  "@types/express": "^5.0.6",
17
17
  "@types/jsonwebtoken": "^9.0.10",
18
18
  "@types/node": "^24.10.1",
19
+ "@types/react": "^19.2.7",
20
+ "@types/react-dom": "^19.2.3",
19
21
  "@vitejs/plugin-react": "^5.1.1",
20
22
  "aaex-file-router": "^1.4.5",
21
23
  "bcrypt": "^6.0.0",
@@ -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) {
6
+ export function render(_url: string, initialData= {}) {
7
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
+ }
@@ -7,9 +7,10 @@ dotenv.config();
7
7
 
8
8
  // server.js is now in .aaex/server/
9
9
  const projectRoot = path.resolve("."); // root of the project
10
-
10
+ let serverRoutes;
11
11
  // Import BuildApiRoutes
12
12
  import * as BuildApiRoutes from "../BuildApiRoutes.js";
13
+ import routeMatcher from "../matchServerRoutes.js";
13
14
 
14
15
  const apiRoutes = BuildApiRoutes.default; // default export
15
16
  const PathToRoute = BuildApiRoutes.pathToRoute; // named export
@@ -37,6 +38,7 @@ if (!isProduction) {
37
38
  root: projectRoot,
38
39
  base,
39
40
  });
41
+ serverRoutes = (await vite.ssrLoadModule("/src/server-routes.ts")).default;
40
42
  app.use(vite.middlewares);
41
43
  } else {
42
44
  const compression = (await import("compression")).default;
@@ -46,6 +48,12 @@ if (!isProduction) {
46
48
  base,
47
49
  sirv(path.join(projectRoot, "dist/client"), { extensions: [] })
48
50
  );
51
+
52
+ const serverRoutesModule = await import(
53
+ pathToFileURL(path.join(projectRoot, "dist/src/server-routes.js")).href
54
+ );
55
+
56
+ serverRoutes = serverRoutesModule.default;
49
57
  }
50
58
 
51
59
  // parse JSON bodies
@@ -105,7 +113,20 @@ app.use(/.*/, async (req, res) => {
105
113
  try {
106
114
  let url = req.originalUrl;
107
115
 
108
- console.log("url", url);
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
+ }
109
130
 
110
131
  let template;
111
132
  let render;
@@ -124,11 +145,25 @@ app.use(/.*/, async (req, res) => {
124
145
  ).render;
125
146
  }
126
147
 
127
- 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);
128
159
 
129
160
  const html = template
130
161
  .replace("<!--app-head-->", rendered.head ?? "")
131
- .replace("<!--app-html-->", rendered.html ?? "");
162
+ .replace("<!--app-html-->", rendered.html ?? "")
163
+ .replace(
164
+ "<!--initial-data-->",
165
+ `<script>window.__INITIAL_DATA__ = ${safeData}</script>`
166
+ );
132
167
 
133
168
  res.status(200).set({ "Content-Type": "text/html" }).send(html);
134
169
  } catch (e) {
package/template/.env CHANGED
@@ -1,4 +1,5 @@
1
1
  # edit to fit your needs
2
2
  MONGO_URI="mongodb://localhost:27018/"
3
3
  DB_NAME="mydatabase"
4
- JWT_SECRET="your-jwt-secret"
4
+ JWT_SECRET="your-jwt-secret"
5
+ JWT_EXP="24h"
@@ -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;
@@ -1,23 +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 type { RouteObject } from 'react-router-dom';
7
-
8
- const routes: RouteObject[] = [
9
- {
10
- "path": "",
11
- "element": React.createElement(Index)
12
- },
13
- {
14
- "path": "login",
15
- "element": React.createElement(Login)
16
- },
17
- {
18
- "path": "register",
19
- "element": React.createElement(Register)
20
- }
21
- ];
22
-
23
- export default routes;
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;
@@ -1,19 +1,50 @@
1
- import { useAuth } from "../hooks/useAuth";
1
+ import { useAuth } from "../hooks/useAuth.js";
2
2
  import { FileLink } from "aaex-file-router";
3
- import { FileRoutes } from "../routeTypes";
3
+ import { FileRoutes } from "../routeTypes.js";
4
4
 
5
- export default function Home() {
6
- const { user, valid, loading } = useAuth();
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();
7
23
 
8
- console.log(user, valid, loading);
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();
9
33
 
10
34
  if (loading) return <p>Checking authentication...</p>;
11
35
  if (!valid)
12
36
  return (
13
37
  <p>
14
- You are not logged in! <br/> <FileLink<FileRoutes> to="/login">Login</FileLink>
38
+ You are not logged in! <br />
39
+ <FileLink<FileRoutes> to="/login">Login</FileLink>
15
40
  </p>
16
41
  );
17
42
 
18
- return <p>Welcome, {user ? user.username : ""}</p>;
43
+ return (
44
+ <>
45
+ <p>Welcome, {user ? user.username : ""} </p>
46
+ <br />
47
+ <p>Server rendered content: {hello}</p>
48
+ </>
49
+ );
19
50
  }
@@ -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
+ }
@@ -1,5 +1,7 @@
1
- // AUTO-GENERATED: DO NOT EDIT
2
- export type FileRoutes =
3
- | "/"
4
- | "/login"
5
- | "/register";
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,9 +1,9 @@
1
1
  import { defineConfig } from "vite";
2
2
  import react from "@vitejs/plugin-react";
3
- import { aaexFileRouter } from "aaex-file-router/plugin";
3
+ import { aaexServerRouter } from "aaex-file-router/plugin";
4
4
  import { pluginSsrDevFoucFix } from "./.aaex/utils/ServerLoadCssImports";
5
5
 
6
6
  // https://vite.dev/config/
7
7
  export default defineConfig({
8
- plugins: [react(), aaexFileRouter(), pluginSsrDevFoucFix()],
8
+ plugins: [react(), aaexServerRouter(), pluginSsrDevFoucFix()],
9
9
  });
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2024",
4
- "module": "esnext",
5
- "declaration": true,
6
- "outDir": "dist",
7
- "strict": true,
8
- "esModuleInterop": true
9
- },
10
- "include": [
11
- ".aaex/api","src/api"
12
- ],
13
- "exclude": [
14
- "node_modules",
15
- "dist"
16
- ]
17
- }