aaex-cli 1.2.0 → 1.4.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/.TODO +4 -0
- package/README.md +4 -3
- package/create-aaex-app.js +1 -1
- package/package.json +3 -1
- package/template/.aaex/framework/entry-client.tsx +6 -6
- package/template/.aaex/framework/entry-server.tsx +2 -5
- package/template/.aaex/matchServerRoutes.js +60 -0
- package/template/.aaex/server/server.js +39 -4
- package/template/.env +2 -1
- package/template/gitignore +3 -0
- package/template/index.html +1 -0
- package/template/src/App.tsx +21 -10
- package/template/src/{routes.ts → client-routes.ts} +33 -23
- package/template/src/pages/index.tsx +38 -7
- package/template/src/pages/test/[slug].tsx +7 -0
- package/template/src/routeTypes.ts +7 -5
- package/template/src/server-routes.ts +37 -0
- package/template/tsconfig.aaex.json +20 -0
- package/template/vite.config.ts +2 -2
- package/template/tsconfig.api.json +0 -17
package/.TODO
ADDED
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.
|
|
19
|
-
Added
|
|
19
|
+
## V1.4
|
|
20
|
+
Added server side data loading
|
package/create-aaex-app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaex-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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, ">")
|
|
153
|
+
// .replace(/'/g, "'")
|
|
154
|
+
// .replace(/"/g, """);
|
|
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
package/template/index.html
CHANGED
package/template/src/App.tsx
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import "./App.css";
|
|
2
|
-
import { Route,
|
|
3
|
-
import
|
|
2
|
+
import { Route, Routes } from "react-router";
|
|
3
|
+
import serverRoutes from "./server-routes";
|
|
4
|
+
import { createElement } from "react";
|
|
4
5
|
|
|
5
|
-
function renderRoutes(routesArray:
|
|
6
|
+
function renderRoutes(routesArray: any[], initialData: any) {
|
|
6
7
|
return routesArray.map((route) => {
|
|
7
|
-
|
|
8
|
+
const element = createElement(route.element as any, { ...initialData });
|
|
9
|
+
|
|
10
|
+
if (route.children?.length) {
|
|
8
11
|
return (
|
|
9
|
-
<Route
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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/>
|
|
38
|
+
You are not logged in! <br />
|
|
39
|
+
<FileLink<FileRoutes> to="/login">Login</FileLink>
|
|
15
40
|
</p>
|
|
16
41
|
);
|
|
17
42
|
|
|
18
|
-
return
|
|
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,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
|
+
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
|
-
import {
|
|
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(),
|
|
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
|
-
}
|