@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 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
 
@@ -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.0.0",
4
- "description": "Nexa Stack — Node.js framework. One resource REST API, ORM, auth, queue, and admin panel. Zero native build (Hostinger / Windows friendly).",
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
- "prepublishOnly": "vitest run"
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
@@ -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("Dev bar open /admin or /docs (bottom toolbar)");
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 page = path === "/" ? "index" : path.slice(1);
602
- if (!page.includes("..") && /^[a-zA-Z0-9/_-]*$/.test(page)) {
603
- for (const dir of this.staticDirs()) {
604
- const full = join(dir, `${page}.html`);
605
- if (existsSync(full)) {
606
- return this.serveHtmlPage(full);
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") {