aaex-cli 1.4.1 → 2.0.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/.env +1 -0
- package/README.md +12 -5
- package/create-aaex-app.js +41 -29
- package/package.json +8 -3
- package/template/src/hooks/useAuth.tsx +1 -1
- package/template/src/pages/login.tsx +1 -1
- package/template/{tsconfig.aaex.json → tsconfig.json} +2 -2
- package/template/vite.config.ts +1 -1
- package/template/.aaex/BuildApiRoutes.js +0 -67
- package/template/.aaex/api/auth/login.ts +0 -57
- package/template/.aaex/api/auth/register.ts +0 -34
- package/template/.aaex/api/auth/validate.ts +0 -18
- package/template/.aaex/framework/database/mongodb.ts +0 -29
- package/template/.aaex/framework/entry-client.tsx +0 -16
- package/template/.aaex/framework/entry-server.tsx +0 -18
- package/template/.aaex/matchServerRoutes.js +0 -60
- package/template/.aaex/server/server.js +0 -179
- package/template/.aaex/utils/ServerLoadCssImports.ts +0 -51
- package/template/.aaex/utils/cookies.ts +0 -63
- package/template/.env +0 -5
- package/template/gitignore +0 -3
- package/template/src/client-routes.ts +0 -33
- package/template/src/routeTypes.ts +0 -7
- package/template/src/server-routes.ts +0 -37
- /package/template/{src/vite-env.d.ts → vite-env.d.ts} +0 -0
package/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AAEX_SKIP_POSTINSTALL=true
|
package/README.md
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# AaExJS
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
## V.2.0.1
|
|
4
|
+
|
|
5
|
+
Bug fix for css modules
|
|
6
|
+
|
|
3
7
|
## Description
|
|
8
|
+
|
|
4
9
|
Light weight SSR framework for react with filebased page and api routing. Builds on Vitejs alreadt existing SSR functionality.
|
|
5
10
|
|
|
6
11
|
## Features
|
|
12
|
+
|
|
7
13
|
- File routing using aaex-file-router (can be used seperatly)
|
|
8
14
|
- API routing using hybrid solution only available in the full framework
|
|
9
15
|
- SSR rendering using vites native functions + additional functionality
|
|
10
16
|
- full typescript support (currently only typescript)
|
|
11
17
|
- all vite plugins that work with ssr should work
|
|
12
18
|
|
|
19
|
+
## Known issues:
|
|
20
|
+
|
|
21
|
+
- tailwindcss dosnt work as excpected
|
|
13
22
|
|
|
14
23
|
## Usage
|
|
24
|
+
|
|
15
25
|
```sh
|
|
16
26
|
npx create-aaex-app <project-name>
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## V1.4
|
|
20
|
-
Added server side data loading
|
|
27
|
+
```
|
package/create-aaex-app.js
CHANGED
|
@@ -3,51 +3,41 @@
|
|
|
3
3
|
import fs from "fs-extra";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import chalk from "chalk";
|
|
6
8
|
|
|
7
|
-
//
|
|
8
|
-
// Helpers for ESM __dirname
|
|
9
|
-
// -------------------------
|
|
9
|
+
// ESM __dirname
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
|
|
13
|
-
// -------------------------
|
|
14
13
|
// Parse CLI arguments
|
|
15
|
-
// -------------------------
|
|
16
14
|
const args = process.argv.slice(2);
|
|
17
15
|
if (!args[0]) {
|
|
18
|
-
console.error("Usage: create-aaex-app <project-name>");
|
|
16
|
+
console.error(chalk.red("Usage: create-aaex-app <project-name>"));
|
|
19
17
|
process.exit(1);
|
|
20
18
|
}
|
|
21
19
|
const appName = args[0];
|
|
22
20
|
const targetDir = path.resolve(process.cwd(), appName);
|
|
23
21
|
|
|
24
|
-
//
|
|
25
|
-
// Paths inside the package
|
|
26
|
-
// -------------------------
|
|
22
|
+
// Paths inside package
|
|
27
23
|
const templateDir = path.resolve(__dirname, "template");
|
|
28
24
|
|
|
29
|
-
// -------------------------
|
|
30
25
|
// Copy template files
|
|
31
|
-
|
|
32
|
-
async function copyTemplate() {
|
|
26
|
+
async function copyTemplate(database = "MongoDB") {
|
|
33
27
|
try {
|
|
34
|
-
console.log(
|
|
35
|
-
|
|
28
|
+
console.log(chalk.blue(`\nCreating new AAEx app in ${targetDir}...\n`));
|
|
29
|
+
console.log(chalk.cyan(`With ${database} as DB driver`));
|
|
36
30
|
await fs.copy(templateDir, targetDir, {
|
|
37
31
|
overwrite: true,
|
|
38
32
|
errorOnExist: false,
|
|
39
33
|
});
|
|
40
|
-
|
|
41
|
-
console.log("Template copied successfully.");
|
|
42
34
|
} catch (err) {
|
|
43
|
-
console.error("Error copying template:", err);
|
|
35
|
+
console.error(chalk.red("❌ Error copying template:"), err);
|
|
44
36
|
process.exit(1);
|
|
45
37
|
}
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
// -------------------------
|
|
49
40
|
// Create package.json
|
|
50
|
-
// -------------------------
|
|
51
41
|
async function createPackageJson() {
|
|
52
42
|
const pkg = {
|
|
53
43
|
name: appName,
|
|
@@ -73,11 +63,12 @@ async function createPackageJson() {
|
|
|
73
63
|
express: "^5.1.0",
|
|
74
64
|
compression: "^1.8.1",
|
|
75
65
|
sirv: "^3.0.2",
|
|
76
|
-
"aaex-file-router": "^
|
|
66
|
+
"aaex-file-router": "^2.0.0",
|
|
77
67
|
jsonwebtoken: "^9.0.3",
|
|
78
68
|
mongodb: "^7.0.0",
|
|
79
69
|
bcrypt: "^6.0.0",
|
|
80
70
|
dotenv: "^17.2.3",
|
|
71
|
+
"aaexjs": "^2.0.0"
|
|
81
72
|
},
|
|
82
73
|
devDependencies: {
|
|
83
74
|
typescript: "~5.9.2",
|
|
@@ -96,27 +87,48 @@ async function createPackageJson() {
|
|
|
96
87
|
await fs.writeJson(path.join(targetDir, "package.json"), pkg, {
|
|
97
88
|
spaces: 2,
|
|
98
89
|
});
|
|
99
|
-
console.log("package.json created.");
|
|
100
90
|
} catch (err) {
|
|
101
|
-
console.error("Error creating package.json:", err);
|
|
91
|
+
console.error(chalk.red("Error creating package.json:"), err);
|
|
102
92
|
process.exit(1);
|
|
103
93
|
}
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
//
|
|
107
|
-
// Run
|
|
108
|
-
// -------------------------
|
|
96
|
+
// Main
|
|
109
97
|
async function main() {
|
|
110
98
|
if (fs.existsSync(targetDir)) {
|
|
111
|
-
console.error(`Folder ${appName} already exists. Aborting.`);
|
|
99
|
+
console.error(chalk.red(`Folder ${appName} already exists. Aborting.`));
|
|
112
100
|
process.exit(1);
|
|
113
101
|
}
|
|
114
102
|
|
|
115
|
-
|
|
103
|
+
// Interaktiv fråga
|
|
104
|
+
const answers = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: "select",
|
|
107
|
+
name: "database",
|
|
108
|
+
message: "Which database do you want to use?",
|
|
109
|
+
choices: [
|
|
110
|
+
"MongoDB",
|
|
111
|
+
"MySQL - not implemented",
|
|
112
|
+
"PostgreSQL - not implemented",
|
|
113
|
+
],
|
|
114
|
+
default: 0,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (answers.database !== "MongoDB") {
|
|
119
|
+
console.log(
|
|
120
|
+
chalk.yellow(`Currently only MongoDB is supported. Aborting...`)
|
|
121
|
+
);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await copyTemplate(answers.database);
|
|
116
126
|
await createPackageJson();
|
|
117
127
|
|
|
118
|
-
console.log("\
|
|
119
|
-
console.log(
|
|
128
|
+
console.log(chalk.green.bold("\nAAEx app created successfully!"));
|
|
129
|
+
console.log(
|
|
130
|
+
chalk.blue(`\nNext steps:\n cd ${appName}\n npm install\n npm run dev\n`)
|
|
131
|
+
);
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
main();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaex-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Command line interface for creating aaexjs app",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"create-aaex-app": "
|
|
9
|
+
"create-aaex-app": "create-aaex-app.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"vite": "^7.2.6"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
34
|
+
"chalk": "^5.6.2",
|
|
35
|
+
"fs-extra": "^11.3.2",
|
|
36
|
+
"inquirer": "^13.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"aaexjs": "2.0.1"
|
|
35
40
|
}
|
|
36
41
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
import { aaexServerRouter } from "aaex-file-router/plugin";
|
|
4
|
-
import { pluginSsrDevFoucFix } from "
|
|
4
|
+
import { pluginSsrDevFoucFix } from "aaexjs-test";
|
|
5
5
|
|
|
6
6
|
// https://vite.dev/config/
|
|
7
7
|
export default defineConfig({
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { FileScanner } from "aaex-file-router/core";
|
|
2
|
-
import { match } from "path-to-regexp";
|
|
3
|
-
|
|
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";
|
|
12
|
-
|
|
13
|
-
const internalApiFolder = ".aaex/api";
|
|
14
|
-
|
|
15
|
-
//collect file data as easily parsed arrays
|
|
16
|
-
const [userFiles, internalFiles] = await Promise.all([
|
|
17
|
-
scanApiFolder(userApiFolder),
|
|
18
|
-
scanApiFolder(internalApiFolder),
|
|
19
|
-
]);
|
|
20
|
-
|
|
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());
|
|
26
|
-
|
|
27
|
-
// --- build route list ---
|
|
28
|
-
/**Builds route object from the scanned files */
|
|
29
|
-
function buildApiRoutes(files) {
|
|
30
|
-
const routes = [];
|
|
31
|
-
|
|
32
|
-
function walk(node, currentPath) {
|
|
33
|
-
//recursivly iterate over child routes
|
|
34
|
-
if (node.isDirectory) {
|
|
35
|
-
node.children?.forEach((c) => walk(c, currentPath + "/" + node.name));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const filePath = currentPath + "/" + node.name;
|
|
39
|
-
//build route path from filePath
|
|
40
|
-
let route = filePath
|
|
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 });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
files.forEach((file) => walk(file, ""));
|
|
50
|
-
return routes;
|
|
51
|
-
}
|
|
52
|
-
|
|
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) {
|
|
58
|
-
for (const r of routes) {
|
|
59
|
-
const matcher = match(r.route, { decode: decodeURIComponent });
|
|
60
|
-
const matched = matcher(pathname);
|
|
61
|
-
if (matched) return { route: r, params: matched.params };
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// --- export ---
|
|
67
|
-
export { routes, pathToRoute };
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
1
|
-
import "../../src/index.css";
|
|
2
|
-
import { StrictMode } from "react";
|
|
3
|
-
import { hydrateRoot } from "react-dom/client";
|
|
4
|
-
import App from "../../src/App";
|
|
5
|
-
import { BrowserRouter } from "react-router";
|
|
6
|
-
|
|
7
|
-
const initialData = (window as any).__INITIAL_DATA__;
|
|
8
|
-
|
|
9
|
-
hydrateRoot(
|
|
10
|
-
document.getElementById("root") as HTMLElement,
|
|
11
|
-
<StrictMode>
|
|
12
|
-
<BrowserRouter>
|
|
13
|
-
<App initialData={initialData} />
|
|
14
|
-
</BrowserRouter>
|
|
15
|
-
</StrictMode>
|
|
16
|
-
);
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { StrictMode, Suspense } from "react";
|
|
2
|
-
import { renderToString } from "react-dom/server";
|
|
3
|
-
import { StaticRouter } from "react-router";
|
|
4
|
-
import App from "../../src/App";
|
|
5
|
-
|
|
6
|
-
export function render(_url: string, initialData= {}) {
|
|
7
|
-
const url = `${_url}`;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const html = renderToString(
|
|
11
|
-
<StrictMode>
|
|
12
|
-
<StaticRouter location={url}>
|
|
13
|
-
<App initialData={initialData} />
|
|
14
|
-
</StaticRouter>
|
|
15
|
-
</StrictMode>
|
|
16
|
-
);
|
|
17
|
-
return { html };
|
|
18
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import dotenv from "dotenv";
|
|
6
|
-
dotenv.config();
|
|
7
|
-
|
|
8
|
-
// server.js is now in .aaex/server/
|
|
9
|
-
const projectRoot = path.resolve("."); // root of the project
|
|
10
|
-
let serverRoutes;
|
|
11
|
-
// Import BuildApiRoutes
|
|
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
|
|
17
|
-
|
|
18
|
-
// Constants
|
|
19
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
20
|
-
const port = process.env.PORT || 5173;
|
|
21
|
-
const base = process.env.BASE || "/";
|
|
22
|
-
|
|
23
|
-
// Cached production HTML
|
|
24
|
-
const templateHtml = isProduction
|
|
25
|
-
? await fs.readFile(path.join(projectRoot, "dist/client/index.html"), "utf-8")
|
|
26
|
-
: "";
|
|
27
|
-
|
|
28
|
-
// Create HTTP server
|
|
29
|
-
const app = express();
|
|
30
|
-
|
|
31
|
-
/** @type {import('vite').ViteDevServer | undefined} */
|
|
32
|
-
let vite;
|
|
33
|
-
if (!isProduction) {
|
|
34
|
-
const { createServer } = await import("vite");
|
|
35
|
-
vite = await createServer({
|
|
36
|
-
server: { middlewareMode: true },
|
|
37
|
-
appType: "custom",
|
|
38
|
-
root: projectRoot,
|
|
39
|
-
base,
|
|
40
|
-
});
|
|
41
|
-
serverRoutes = (await vite.ssrLoadModule("/src/server-routes.ts")).default;
|
|
42
|
-
app.use(vite.middlewares);
|
|
43
|
-
} else {
|
|
44
|
-
const compression = (await import("compression")).default;
|
|
45
|
-
const sirv = (await import("sirv")).default;
|
|
46
|
-
app.use(compression());
|
|
47
|
-
app.use(
|
|
48
|
-
base,
|
|
49
|
-
sirv(path.join(projectRoot, "dist/client"), { extensions: [] })
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const serverRoutesModule = await import(
|
|
53
|
-
pathToFileURL(path.join(projectRoot, "dist/src/server-routes.js")).href
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
serverRoutes = serverRoutesModule.default;
|
|
57
|
-
}
|
|
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
|
-
|
|
65
|
-
// API routing
|
|
66
|
-
app.use("/api", async (req, res) => {
|
|
67
|
-
const routeMatch = PathToRoute(req.path, apiRoutes);
|
|
68
|
-
|
|
69
|
-
if (!routeMatch)
|
|
70
|
-
return res.status(404).json({ error: "API route not found" });
|
|
71
|
-
|
|
72
|
-
const { route, params } = routeMatch;
|
|
73
|
-
let modulePath;
|
|
74
|
-
|
|
75
|
-
if (!isProduction) {
|
|
76
|
-
// DEV: Vite handles TS/JS loading
|
|
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
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
// PROD: bundled JS
|
|
84
|
-
modulePath = pathToFileURL(
|
|
85
|
-
`./dist/server/api${route.filePath
|
|
86
|
-
.replace(/^src\/api/, "")
|
|
87
|
-
.replace(/\.ts$/, ".js")}`
|
|
88
|
-
).href;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const mod = !isProduction
|
|
93
|
-
? await vite.ssrLoadModule(modulePath)
|
|
94
|
-
: await import(modulePath);
|
|
95
|
-
|
|
96
|
-
const handler = mod[req.method]; // GET, POST, etc.
|
|
97
|
-
|
|
98
|
-
if (typeof handler !== "function") {
|
|
99
|
-
return res.status(405).json({ error: "Method not allowed" });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const result = await handler(req, res, params);
|
|
103
|
-
|
|
104
|
-
if (!res.headersSent) res.json(result);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
console.error("API load error:", err);
|
|
107
|
-
res.status(500).json({ error: "Internal Server Error" });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// SSR HTML
|
|
112
|
-
app.use(/.*/, async (req, res) => {
|
|
113
|
-
try {
|
|
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
|
-
}
|
|
130
|
-
|
|
131
|
-
let template;
|
|
132
|
-
let render;
|
|
133
|
-
if (!isProduction) {
|
|
134
|
-
template = await fs.readFile(
|
|
135
|
-
path.join(projectRoot, "index.html"),
|
|
136
|
-
"utf-8"
|
|
137
|
-
);
|
|
138
|
-
template = await vite.transformIndexHtml(url, template);
|
|
139
|
-
render = (await vite.ssrLoadModule("/.aaex/framework/entry-server.tsx"))
|
|
140
|
-
.render;
|
|
141
|
-
} else {
|
|
142
|
-
template = templateHtml;
|
|
143
|
-
render = (
|
|
144
|
-
await import(path.join(projectRoot, "dist/server/entry-server.js"))
|
|
145
|
-
).render;
|
|
146
|
-
}
|
|
147
|
-
|
|
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);
|
|
159
|
-
|
|
160
|
-
const html = template
|
|
161
|
-
.replace("<!--app-head-->", rendered.head ?? "")
|
|
162
|
-
.replace("<!--app-html-->", rendered.html ?? "")
|
|
163
|
-
.replace(
|
|
164
|
-
"<!--initial-data-->",
|
|
165
|
-
`<script>window.__INITIAL_DATA__ = ${safeData}</script>`
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
res.status(200).set({ "Content-Type": "text/html" }).send(html);
|
|
169
|
-
} catch (e) {
|
|
170
|
-
vite?.ssrFixStacktrace?.(e);
|
|
171
|
-
console.error(e);
|
|
172
|
-
res.status(500).send(e.stack);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Start server
|
|
177
|
-
app.listen(port, () => {
|
|
178
|
-
console.log(`Server started at http://localhost:${port}`);
|
|
179
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
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
DELETED
package/template/gitignore
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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,37 +0,0 @@
|
|
|
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;
|
|
File without changes
|