@nexa-stack/framework 1.0.0 → 1.1.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/README.md +13 -3
- package/bin/nexa.ts +25 -11
- package/docs/UI_RULES.md +19 -0
- package/package.json +11 -5
- package/packages/core/src/app.ts +1 -1
- package/packages/server/src/router.ts +36 -12
- package/public/admin-assets/nexa.css +2 -0
- package/public/admin-assets/nexa.umd.js +80 -0
- package/public/admin.html +6 -4
- package/public/app.html +19 -0
- package/public/index.html +22 -66
- package/public/compare.html +0 -66
- package/public/docs.html +0 -315
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
# Nexa
|
|
2
|
-
|
|
3
|
-
Write little → ship a full backend. Goal: **hours, not weeks.**
|
|
1
|
+
# Nexa
|
|
2
|
+
|
|
3
|
+
Write little → ship a full backend. Goal: **hours, not weeks.**
|
|
4
|
+
|
|
5
|
+
Nexa Stack is the backend package. Its UI companion is a separate package:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {nexa} from '@nexa-stack/ui';
|
|
9
|
+
import '@nexa-stack/ui/styles.css';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Nexa Stack serves a minimal Welcome page, REST API and a schema-driven admin at `/admin`.
|
|
13
|
+
For custom pages, use the independent `@nexa-stack/ui` package and `nexa="..."` commands.
|
|
4
14
|
|
|
5
15
|
Runs on **Node.js ≥ 20** (Hostinger-friendly). Default UI language is **English**.
|
|
6
16
|
|
package/bin/nexa.ts
CHANGED
|
@@ -8,7 +8,9 @@ const here = moduleDir(import.meta.url);
|
|
|
8
8
|
const frameworkRoot = join(here, "..").replace(/\\/g, "/");
|
|
9
9
|
const frameworkPkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
|
|
10
10
|
const PKG = frameworkPkg.name as string;
|
|
11
|
-
const PKG_VERSION = frameworkPkg.version as string;
|
|
11
|
+
const PKG_VERSION = frameworkPkg.version as string;
|
|
12
|
+
const UI_PKG = "@nexa-stack/ui";
|
|
13
|
+
const UI_VERSION = "^1.1.0";
|
|
12
14
|
|
|
13
15
|
const [command, ...args] = process.argv.slice(2);
|
|
14
16
|
const arg = args[0];
|
|
@@ -76,7 +78,7 @@ if (command === "serve" || command === "run") {
|
|
|
76
78
|
start: "nexa serve",
|
|
77
79
|
nexa: "nexa",
|
|
78
80
|
},
|
|
79
|
-
dependencies: { [PKG]: depVersion },
|
|
81
|
+
dependencies: { [PKG]: depVersion, [UI_PKG]: UI_VERSION },
|
|
80
82
|
engines: { node: ">=20" },
|
|
81
83
|
},
|
|
82
84
|
null,
|
|
@@ -235,18 +237,30 @@ APP_SECRET=random-string-at-least-32-chars
|
|
|
235
237
|
)
|
|
236
238
|
);
|
|
237
239
|
const adminSrc = join(frameworkPath, "public/admin.html");
|
|
238
|
-
if (existsSync(adminSrc)) {
|
|
239
|
-
cpSync(adminSrc, join(dir, "public/admin.html"));
|
|
240
|
-
}
|
|
240
|
+
if (existsSync(adminSrc)) {
|
|
241
|
+
cpSync(adminSrc, join(dir, "public/admin.html"));
|
|
242
|
+
}
|
|
243
|
+
const appSrc = join(frameworkPath, "public/app.html");
|
|
244
|
+
if (existsSync(appSrc)) {
|
|
245
|
+
cpSync(appSrc, join(dir, "public/app.html"));
|
|
246
|
+
}
|
|
247
|
+
const adminAssetsSrc = join(frameworkPath, "public/admin-assets");
|
|
248
|
+
if (existsSync(adminAssetsSrc)) {
|
|
249
|
+
cpSync(adminAssetsSrc, join(dir, "public/admin-assets"), { recursive: true });
|
|
250
|
+
}
|
|
241
251
|
const indexSrc = join(frameworkPath, "public/index.html");
|
|
242
|
-
if (existsSync(indexSrc)) {
|
|
243
|
-
cpSync(indexSrc, join(dir, "public/index.html"));
|
|
244
|
-
} else {
|
|
252
|
+
if (existsSync(indexSrc)) {
|
|
253
|
+
cpSync(indexSrc, join(dir, "public/index.html"));
|
|
254
|
+
} else {
|
|
245
255
|
writeFileSync(
|
|
246
256
|
join(dir, "public/index.html"),
|
|
247
|
-
`<!DOCTYPE html><meta http-equiv="refresh" content="0;url=/admin"><a href="/admin">Admin</a>`
|
|
248
|
-
);
|
|
249
|
-
}
|
|
257
|
+
`<!DOCTYPE html><meta http-equiv="refresh" content="0;url=/admin"><a href="/admin">Admin</a>`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const uiRulesSrc = join(frameworkPath, "docs/UI_RULES.md");
|
|
261
|
+
if (existsSync(uiRulesSrc)) {
|
|
262
|
+
cpSync(uiRulesSrc, join(dir, "NEXA_UI.md"));
|
|
263
|
+
}
|
|
250
264
|
console.log(`
|
|
251
265
|
Project ready: ${name}
|
|
252
266
|
|
package/docs/UI_RULES.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Nexa UI rules
|
|
2
|
+
|
|
3
|
+
Every custom interface page in a Nexa project must use `@nexa-stack/ui` and `nexa="..."` commands.
|
|
4
|
+
|
|
5
|
+
- Start from `public/app.html` and load `/admin-assets/nexa.css` plus `/admin-assets/nexa.umd.js`.
|
|
6
|
+
- Use existing Nexa components for layout, cards, fields, buttons, tables, modal and toast.
|
|
7
|
+
- Do not add replacement `.card`, `.button`, `.input` or layout CSS to a page.
|
|
8
|
+
- Keep page JavaScript for state, API calls and event handling only. Nexa renders the visual components.
|
|
9
|
+
- If a missing visual pattern is reusable, add it as a Nexa UI recipe; do not create one-off CSS.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<body nexa="luxury light azure">
|
|
15
|
+
<header nexa="header glass fixed"></header>
|
|
16
|
+
<section nexa="hero split"></section>
|
|
17
|
+
<section nexa="features cards-3"></section>
|
|
18
|
+
</body>
|
|
19
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexa-stack/framework",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Nexa Stack —
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Nexa Stack — a schema-driven TypeScript backend with REST, JWT authentication and a Nexa UI-powered admin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./packages/core/src/index.ts",
|
|
7
7
|
"module": "./packages/core/src/index.ts",
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
"dev": "nexa serve",
|
|
31
31
|
"nexa": "node --import tsx bin/nexa.ts",
|
|
32
32
|
"serve": "node --import tsx bin/nexa.ts serve",
|
|
33
|
+
"build": "echo Nexa Stack ships a prebuilt admin shell.",
|
|
33
34
|
"test": "vitest run",
|
|
34
|
-
"
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"prepublishOnly": "npm test && npm run build"
|
|
35
37
|
},
|
|
36
38
|
"keywords": [
|
|
37
39
|
"nexa",
|
|
@@ -59,7 +61,8 @@
|
|
|
59
61
|
"nodemailer": "^9.0.6",
|
|
60
62
|
"postgres": "^3.4.9",
|
|
61
63
|
"sql.js": "^1.14.2",
|
|
62
|
-
"tsx": "^4.20.3"
|
|
64
|
+
"tsx": "^4.20.3",
|
|
65
|
+
"@nexa-stack/ui": "^1.1.0"
|
|
63
66
|
},
|
|
64
67
|
"optionalDependencies": {
|
|
65
68
|
"ioredis": "^5.6.1"
|
|
@@ -69,7 +72,10 @@
|
|
|
69
72
|
"@types/node": "^22.15.30",
|
|
70
73
|
"@types/nodemailer": "^8.0.1",
|
|
71
74
|
"@types/sql.js": "^1.4.11",
|
|
72
|
-
"vitest": "^3.2.3"
|
|
75
|
+
"vitest": "^3.2.3",
|
|
76
|
+
"eslint": "9.39.4",
|
|
77
|
+
"typescript": "5.9.3",
|
|
78
|
+
"vite": "8.0.13"
|
|
73
79
|
},
|
|
74
80
|
"allowScripts": {
|
|
75
81
|
"esbuild@0.28.2": true
|
package/packages/core/src/app.ts
CHANGED
|
@@ -62,7 +62,7 @@ export async function start(dbPath?: string, port?: number) {
|
|
|
62
62
|
setDevPort(server.port);
|
|
63
63
|
log.info(`Nexa → http://localhost:${server.port} (${dbInstance.dialect})`);
|
|
64
64
|
if (config("NODE_ENV") !== "production") {
|
|
65
|
-
log.info("
|
|
65
|
+
log.info("Open / for Welcome, /admin for the schema-driven admin, or /app for the Nexa UI starter.");
|
|
66
66
|
}
|
|
67
67
|
return server;
|
|
68
68
|
}
|
|
@@ -475,7 +475,7 @@ export class Router {
|
|
|
475
475
|
return [this.staticDir, this.fallbackStaticDir].filter(Boolean) as string[];
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
private async servePublicFile(path: string, contentType: string): Promise<Response | null> {
|
|
478
|
+
private async servePublicFile(path: string, contentType: string): Promise<Response | null> {
|
|
479
479
|
for (const dir of this.staticDirs()) {
|
|
480
480
|
const full = join(dir, path);
|
|
481
481
|
if (!existsSync(full)) continue;
|
|
@@ -483,7 +483,27 @@ export class Router {
|
|
|
483
483
|
return new Response(buf, { headers: { "Content-Type": contentType } });
|
|
484
484
|
}
|
|
485
485
|
return null;
|
|
486
|
-
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Serve bundled frontend assets (Vite emits hashed JS/CSS/fonts beside HTML). */
|
|
489
|
+
private async serveStaticAsset(path: string): Promise<Response | null> {
|
|
490
|
+
if (path.includes("..") || !/^[a-zA-Z0-9@._/-]+$/.test(path)) return null;
|
|
491
|
+
const extension = extname(path).toLowerCase();
|
|
492
|
+
const mime: Record<string, string> = {
|
|
493
|
+
".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8",
|
|
494
|
+
".mjs": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8",
|
|
495
|
+
".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg",
|
|
496
|
+
".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif",
|
|
497
|
+
".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2",
|
|
498
|
+
};
|
|
499
|
+
const contentType = mime[extension];
|
|
500
|
+
if (!contentType) return null;
|
|
501
|
+
const file = await this.servePublicFile(path, contentType);
|
|
502
|
+
if (!file) return null;
|
|
503
|
+
const headers = new Headers(file.headers);
|
|
504
|
+
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
505
|
+
return new Response(file.body, { status: file.status, headers });
|
|
506
|
+
}
|
|
487
507
|
|
|
488
508
|
private async serveHtmlPage(fullPath: string): Promise<Response> {
|
|
489
509
|
let html = await readFile(fullPath, "utf8");
|
|
@@ -597,16 +617,20 @@ export class Router {
|
|
|
597
617
|
return new Response(buf);
|
|
598
618
|
}
|
|
599
619
|
|
|
600
|
-
if (req.method === "GET" && !path.startsWith("/api")) {
|
|
601
|
-
const
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
620
|
+
if (req.method === "GET" && !path.startsWith("/api")) {
|
|
621
|
+
const asset = await this.serveStaticAsset(path.slice(1));
|
|
622
|
+
if (asset) return asset;
|
|
623
|
+
const page = path === "/" ? "index" : path.slice(1);
|
|
624
|
+
if (!page.includes("..") && /^[a-zA-Z0-9/_-]*$/.test(page)) {
|
|
625
|
+
for (const dir of this.staticDirs()) {
|
|
626
|
+
const full = join(dir, `${page}.html`);
|
|
627
|
+
if (existsSync(full)) {
|
|
628
|
+
return this.serveHtmlPage(full);
|
|
629
|
+
}
|
|
630
|
+
const directoryIndex = join(dir, page, "index.html");
|
|
631
|
+
if (existsSync(directoryIndex)) return this.serveHtmlPage(directoryIndex);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
610
634
|
}
|
|
611
635
|
|
|
612
636
|
if (req.method === "GET" && path === "/api/docs") {
|