@njinlabs/njin 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/package.json +59 -0
  4. package/src/cli/build.ts +162 -0
  5. package/src/cli/dev.ts +11 -0
  6. package/src/cli/index.ts +21 -0
  7. package/src/cli/start.ts +9 -0
  8. package/src/config/module.ts +34 -0
  9. package/src/core/adapters/bun_filesystem.ts +37 -0
  10. package/src/core/adapters/s3.ts +60 -0
  11. package/src/core/banner.ts +54 -0
  12. package/src/core/boot.ts +10 -0
  13. package/src/core/config.ts +89 -0
  14. package/src/core/http_error.ts +21 -0
  15. package/src/core/model/data_type/array.ts +22 -0
  16. package/src/core/model/data_type/boolean.ts +15 -0
  17. package/src/core/model/data_type/date.ts +28 -0
  18. package/src/core/model/data_type/email.ts +21 -0
  19. package/src/core/model/data_type/file.ts +15 -0
  20. package/src/core/model/data_type/index.ts +13 -0
  21. package/src/core/model/data_type/multi_file.ts +18 -0
  22. package/src/core/model/data_type/numeric.ts +15 -0
  23. package/src/core/model/data_type/object.ts +15 -0
  24. package/src/core/model/data_type/relation.ts +73 -0
  25. package/src/core/model/data_type/relation_many.ts +55 -0
  26. package/src/core/model/data_type/richtext.ts +20 -0
  27. package/src/core/model/data_type/select.ts +15 -0
  28. package/src/core/model/data_type/text.ts +20 -0
  29. package/src/core/model/index.ts +244 -0
  30. package/src/core/module/index.ts +12 -0
  31. package/src/core/path_guard.ts +9 -0
  32. package/src/models/file.ts +17 -0
  33. package/src/models/user.ts +18 -0
  34. package/src/modules/admin.ts +43 -0
  35. package/src/modules/analytics.ts +212 -0
  36. package/src/modules/api.ts +160 -0
  37. package/src/modules/auth.ts +153 -0
  38. package/src/modules/elysia.ts +85 -0
  39. package/src/modules/file.ts +131 -0
  40. package/src/modules/img.ts +137 -0
  41. package/src/modules/logger.ts +18 -0
  42. package/src/modules/setup.ts +89 -0
  43. package/src/modules/surreal.ts +58 -0
  44. package/src/modules/users.ts +94 -0
  45. package/src/modules/view.ts +247 -0
  46. package/src/spin.ts +3 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PT Njin Daya Teknologi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # njin
2
+
3
+ A modern framework for building company profiles, landing pages, and content-driven websites. Define your data model once — get a full REST API, admin panel schema, and server-rendered website out of the box.
4
+
5
+ ## Stack
6
+
7
+ - **[Bun](https://bun.sh)** — runtime, package manager, bundler (njin is Bun-only — no Node/Deno support)
8
+ - **[Elysia](https://elysiajs.com)** — HTTP framework with type-safe CRUD generation
9
+ - **[SurrealDB](https://surrealdb.com)** — embedded multi-model database (no separate server)
10
+ - **[EdgeJS](https://edgejs.dev)** — server-side template engine with file-based routing
11
+ - **[Vite](https://vitejs.dev)** + **[Tailwind CSS v4](https://tailwindcss.com)** + **[Alpine.js](https://alpinejs.dev)** — frontend tooling
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ bun create njin-app my-web
17
+ cd my-web
18
+ bunx njin dev
19
+ ```
20
+
21
+ ## Project structure
22
+
23
+ ```
24
+ config.ts ← registers models, db, file adapter, etc. (see below)
25
+ src/
26
+ models/ ← your data models
27
+ views/
28
+ layouts/ ← base layout templates
29
+ pages/ ← file-based routes (.edge files)
30
+ components/ ← reusable .edge components
31
+ client/
32
+ main.ts ← frontend entry point
33
+ app.css ← Tailwind CSS
34
+ _admin/ ← optional — drop a built admin panel SPA here (not bundled with njin)
35
+ vite.config.ts ← Vite config
36
+ ```
37
+
38
+ `@njinlabs/njin` itself lives in `node_modules/@njinlabs/njin` — there's no framework source to look at or modify inside your project.
39
+
40
+ ## Defining a model
41
+
42
+ ```ts
43
+ // src/models/post.ts
44
+ import { makeModel, text, date, select } from "@njinlabs/njin";
45
+ import z from "zod";
46
+
47
+ const post = makeModel("post", {
48
+ name: "Post",
49
+ searchFields: ["title"],
50
+ schema: z.object({
51
+ title: text({ label: "Title" }),
52
+ body: text({ label: "Body" }),
53
+ status: select({ label: "Status" }, ["DRAFT", "PUBLISH"]),
54
+ publishedAt: date({ label: "Published At" }),
55
+ }),
56
+ });
57
+
58
+ export default post;
59
+ ```
60
+
61
+ Register it in `config.ts` at the project root:
62
+
63
+ ```ts
64
+ // config.ts
65
+ import { defineConfig } from "@njinlabs/njin/config";
66
+
67
+ export default defineConfig({
68
+ models: [
69
+ () => import("./src/models/post"),
70
+ ],
71
+ });
72
+ ```
73
+
74
+ This automatically generates:
75
+
76
+ | Method | Endpoint | Description |
77
+ |--------|----------|--------------|
78
+ | `GET` | `/api/post` | List with pagination, search, filter, sort |
79
+ | `POST` | `/api/post` | Create |
80
+ | `GET` | `/api/post/:id` | Show (with relations fetched) |
81
+ | `PUT` | `/api/post/:id` | Update |
82
+ | `DELETE` | `/api/post/:id` | Delete |
83
+ | `GET` | `/api/schema` | Full schema for admin panel |
84
+
85
+ ## File-based routing
86
+
87
+ Files in `src/views/pages/` map to routes automatically:
88
+
89
+ ```
90
+ pages/index.edge → GET /
91
+ pages/about.edge → GET /about
92
+ pages/blog/index.edge → GET /blog
93
+ pages/blog/[slug].edge → GET /blog/:slug
94
+ ```
95
+
96
+ ## Template example
97
+
98
+ Models are available as global async functions in every template:
99
+
100
+ ```edge
101
+ @component('layouts/main')
102
+ @slot('title') Blog @end
103
+
104
+ @slot('main')
105
+ @let(result = await post.read({ filters: { status: 'PUBLISH' }, limit: 10 }))
106
+
107
+ @each(item in result.data)
108
+ <article>
109
+ <h2>{{ item.title }}</h2>
110
+ <time>{{ item.publishedAt }}</time>
111
+ </article>
112
+ @end
113
+ @end
114
+ @end
115
+ ```
116
+
117
+ Available template context:
118
+
119
+ ```edge
120
+ {{ params.slug }} {{-- URL params --}}
121
+ {{ query.page }} {{-- Query string --}}
122
+ {{ request.path }} {{-- Current path, e.g. /blog --}}
123
+ {{ request.url }} {{-- Full URL --}}
124
+ ```
125
+
126
+ ## API query parameters
127
+
128
+ ```
129
+ GET /api/post?page=1&limit=10&sort=publishedAt&order=desc
130
+ GET /api/post?search=hello
131
+ GET /api/post?filters[status][$eq]=PUBLISH
132
+ GET /api/post?filters[title][$contains]=hello
133
+ GET /api/post?populate=author,thumbnail
134
+ GET /api/post?populate=none
135
+ ```
136
+
137
+ ## Setup (first run)
138
+
139
+ On first startup, create the admin user:
140
+
141
+ ```bash
142
+ # Check if setup is needed
143
+ GET /api/setup/status
144
+
145
+ # Create first admin user
146
+ POST /api/setup
147
+ { "name": "Admin", "email": "admin@example.com", "password": "yourpassword" }
148
+ # Returns: { data: { token, user } }
149
+ ```
150
+
151
+ After the first user is created, this endpoint is permanently disabled.
152
+
153
+ ## Auth
154
+
155
+ ```bash
156
+ POST /api/auth/login # { email, password } → { data: { token, user } }
157
+ GET /api/auth/check-token # Bearer <token> → { data: user }
158
+ DELETE /api/auth/logout # Bearer <token>
159
+ ```
160
+
161
+ All `/api/*` endpoints require `Authorization: Bearer <token>`.
162
+
163
+ ## Admin panel
164
+
165
+ njin doesn't bundle an admin panel — `_admin/` at your project root is just a static folder the server looks for at startup (`GET /_admin`). Drop a built admin SPA's `index.html`/assets into it (your own, or one shared by the community) and it's served automatically; if it's missing, the startup banner just notes it wasn't found.
166
+
167
+ ## Configuration
168
+
169
+ Everything that used to be an environment variable now lives in a `config.ts` you write at the project root — njin no longer owns `.env` parsing, so reading from `process.env` (or not) is entirely your call:
170
+
171
+ ```ts
172
+ // config.ts
173
+ import { defineConfig } from "@njinlabs/njin/config";
174
+ import bunFilesystemAdapter from "@njinlabs/njin/adapters/bun_filesystem";
175
+
176
+ export default defineConfig({
177
+ port: Number(process.env.PORT ?? 3000),
178
+ db: {
179
+ path: process.env.DB_PATH ?? "rocksdb://data",
180
+ namespace: process.env.DB_NAMESPACE ?? "general",
181
+ database: process.env.DB_DATABASE ?? "general",
182
+ },
183
+ img: {
184
+ hosts: ["cdn.example.com"], // external hosts allowed for GET /img besides same-origin/localhost
185
+ },
186
+ adapters: {
187
+ file: bunFilesystemAdapter({ dir: "./uploads" }), // default — omit entirely to use this with its own default ("./uploads")
188
+ },
189
+ models: [
190
+ () => import("./src/models/post"),
191
+ ],
192
+ });
193
+ ```
194
+
195
+ To store uploads in S3 (or an S3-compatible service like R2/Spaces/MinIO) instead:
196
+
197
+ ```ts
198
+ import s3Adapter from "@njinlabs/njin/adapters/s3";
199
+
200
+ adapters: {
201
+ file: s3Adapter({
202
+ bucket: process.env.S3_BUCKET!,
203
+ region: process.env.S3_REGION,
204
+ accessKeyId: process.env.S3_ACCESS_KEY_ID,
205
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
206
+ endpoint: process.env.S3_ENDPOINT, // for R2/Spaces/MinIO
207
+ }),
208
+ }
209
+ ```
210
+
211
+ ## Commands
212
+
213
+ ```bash
214
+ bunx njin dev # Start dev server (Elysia + Vite HMR)
215
+ bunx njin build # Build for production -> ./out (public/, _admin/, views/, compiled server)
216
+ bunx njin start # Run from source in production mode (no compile)
217
+ ```
218
+
219
+ Wire these as `package.json` scripts (`"dev": "njin dev"`, etc.) if you'd rather run `bun run dev`.
220
+
221
+ ## License
222
+
223
+ MIT
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@njinlabs/njin",
3
+ "version": "0.1.0",
4
+ "description": "A modern framework for building company profiles, landing pages, and content-driven websites.",
5
+ "type": "module",
6
+ "keywords": ["bun", "elysia", "surrealdb", "edgejs", "cms", "framework"],
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/njinlabs/njin.git"
11
+ },
12
+ "homepage": "https://github.com/njinlabs/njin",
13
+ "engines": {
14
+ "bun": ">=1.3.0"
15
+ },
16
+ "files": ["src", "LICENSE"],
17
+ "scripts": {
18
+ "test": "bun test"
19
+ },
20
+ "bin": {
21
+ "njin": "./src/cli/index.ts"
22
+ },
23
+ "exports": {
24
+ ".": "./src/core/model/index.ts",
25
+ "./config": "./src/core/config.ts",
26
+ "./boot": "./src/core/boot.ts",
27
+ "./banner": "./src/core/banner.ts",
28
+ "./adapters/*": "./src/core/adapters/*.ts"
29
+ },
30
+ "devDependencies": {
31
+ "@tailwindcss/vite": "^4.3.0",
32
+ "@types/alpinejs": "^3.13.11",
33
+ "@types/bun": "latest",
34
+ "@types/geoip-lite": "^1.4.4",
35
+ "alpinejs": "^3.15.12",
36
+ "pino-pretty": "^13.1.3",
37
+ "vite": "^8.0.14",
38
+ "zod": "^4.4.3"
39
+ },
40
+ "peerDependencies": {
41
+ "@tailwindcss/vite": "^4.0.0",
42
+ "alpinejs": "^3.0.0",
43
+ "typescript": "^5",
44
+ "vite": "^8.0.0",
45
+ "zod": "^4.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@elysia/bearer": "^1.4.4",
49
+ "@elysia/cors": "^1.4.2",
50
+ "@paralleldrive/cuid2": "^3.3.0",
51
+ "@surrealdb/node": "^3.0.3",
52
+ "edge.js": "^6.5.1",
53
+ "elysia": "^1.4.28",
54
+ "geoip-lite": "^2.0.3",
55
+ "moment": "^2.30.1",
56
+ "pino": "^10.3.1",
57
+ "surrealdb": "^2.0.3"
58
+ }
59
+ }
@@ -0,0 +1,162 @@
1
+ import type { BunPlugin } from "bun";
2
+ import { existsSync } from "node:fs";
3
+ import { cp, mkdir, rm } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = process.cwd();
8
+ const outDir = join(root, "out");
9
+
10
+ console.log("Building for production...\n");
11
+
12
+ await rm(outDir, { recursive: true, force: true });
13
+ await mkdir(outDir, { recursive: true });
14
+
15
+ // Vite build — outDir is forced here regardless of what the project's own
16
+ // vite.config.ts says, so `njin build` always lands in a predictable place.
17
+ const { build } = await import("vite");
18
+ await build({
19
+ configFile: join(root, "vite.config.ts"),
20
+ build: { outDir: join(outDir, "public"), emptyOutDir: true },
21
+ });
22
+ console.log("✓ Built client assets -> out/public");
23
+
24
+ // view.ts resolves views from process.cwd()/src/views at runtime, so the build
25
+ // output preserves that same relative layout instead of flattening it.
26
+ const viewsDir = join(root, "src/views");
27
+ if (existsSync(viewsDir)) {
28
+ await cp(viewsDir, join(outDir, "src/views"), { recursive: true });
29
+ console.log("✓ Copied views -> out/src/views");
30
+ } else {
31
+ console.log("• No src/views found — skipped");
32
+ }
33
+
34
+ const adminDir = join(root, "_admin");
35
+ if (existsSync(adminDir)) {
36
+ await cp(adminDir, join(outDir, "_admin"), { recursive: true });
37
+ console.log("✓ Copied admin panel -> out/_admin");
38
+ } else {
39
+ console.log("• No _admin found — skipped");
40
+ }
41
+
42
+ // surreal.ts statically imports @surrealdb/node unconditionally, and module.ts always wires
43
+ // up the surreal module, so every compiled binary needs this native binding to boot — even
44
+ // for projects using a remote SurrealDB instance. Its loader resolves the platform .node file
45
+ // via `const require = createRequire(import.meta.url); require("./surrealdb-node.<platform>.node")`
46
+ // — using a local `require` value rather than the literal CJS keyword means Bun's bundler never
47
+ // statically recognizes that call as an import to resolve/embed, so it survives bundling
48
+ // untouched and then fails at runtime in a compiled binary (no real filesystem path next to
49
+ // `import.meta.url` to load it from). So instead of embedding it, the matching platform binary
50
+ // is copied next to the executable as a plain file, and a build plugin patches the loader's
51
+ // source to read from there (via `process.execPath`'s directory) at runtime.
52
+ const NATIVE_FILE: Record<string, string> = {
53
+ "win32-x64": "surrealdb-node.win32-x64-msvc.node",
54
+ "win32-arm64": "surrealdb-node.win32-arm64-msvc.node",
55
+ "darwin-x64": "surrealdb-node.darwin-x64.node",
56
+ "darwin-arm64": "surrealdb-node.darwin-arm64.node",
57
+ // musl isn't detected here (njin build doesn't cross-compile); glibc is assumed on Linux.
58
+ "linux-x64": "surrealdb-node.linux-x64-gnu.node",
59
+ "linux-arm64": "surrealdb-node.linux-arm64-gnu.node",
60
+ };
61
+ const nativeFile = NATIVE_FILE[`${process.platform}-${process.arch}`];
62
+ if (!nativeFile) {
63
+ console.error(`\n✗ Unsupported platform for @surrealdb/node: ${process.platform}-${process.arch}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ const surrealNodeDistDir = dirname(fileURLToPath(import.meta.resolve("@surrealdb/node")));
68
+ const bindingsDir = join(outDir, "bindings");
69
+ await mkdir(bindingsDir, { recursive: true });
70
+ await cp(join(surrealNodeDistDir, nativeFile), join(bindingsDir, nativeFile));
71
+ console.log(`✓ Copied native binding -> out/bindings/${nativeFile}`);
72
+
73
+ const surrealNativePlugin: BunPlugin = {
74
+ name: "surrealdb-native-binding",
75
+ setup(build) {
76
+ build.onLoad({ filter: /surrealdb-node\.mjs$/ }, async (args) => {
77
+ const original = await Bun.file(args.path).text();
78
+ const marker = "const require = createRequire(import.meta.url);";
79
+ if (!original.includes(marker)) {
80
+ throw new Error("@surrealdb/node's loader shape changed — expected createRequire marker not found");
81
+ }
82
+ // Wrap `require` so the host platform's own .node lookup is redirected to the copy in
83
+ // out/bindings/; every other platform's file and the optional sibling npm packages fall
84
+ // through to the real require() and fail exactly as they would unpatched (still recorded
85
+ // in the loader's own loadErrors, so a genuinely missing binding still throws clearly).
86
+ const patched = original.replace(
87
+ marker,
88
+ `const __realRequire = createRequire(import.meta.url);
89
+ const require = (specifier) => {
90
+ if (specifier === ${JSON.stringify(`./${nativeFile}`)}) {
91
+ const path = __realRequire("node:path");
92
+ return __realRequire(path.join(path.dirname(process.execPath), "bindings", ${JSON.stringify(nativeFile)}));
93
+ }
94
+ return __realRequire(specifier);
95
+ };`,
96
+ );
97
+ return { contents: patched, loader: "js" };
98
+ });
99
+ },
100
+ };
101
+
102
+ // Generated build entry — a static, literal import of the project's config.ts so
103
+ // `bun build --compile` can trace and embed it (and the model files it dynamically
104
+ // imports) straight into the binary. loadConfig()'s normal dynamic-file-read path
105
+ // only works when running from source (`njin dev`/`njin start`), not from a
106
+ // standalone compiled executable with no source files alongside it.
107
+ //
108
+ // boot/banner are imported dynamically, AFTER loadConfig(config) — static imports are
109
+ // hoisted and evaluated before this file's own body runs, so a static `import { boot }`
110
+ // would pull in config/module.ts (and every module's init(), which read getConfig()) before
111
+ // loadConfig(config) below ever executes. Same reasoning as dev.ts/start.ts.
112
+ const entryPath = join(root, ".njin-build-entry.ts");
113
+ const configPath = join(root, "config.ts").replace(/\\/g, "/");
114
+
115
+ try {
116
+ await Bun.write(
117
+ entryPath,
118
+ `import config from ${JSON.stringify(configPath)};
119
+ import { loadConfig } from "@njinlabs/njin/config";
120
+
121
+ await loadConfig(config);
122
+
123
+ const { boot } = await import("@njinlabs/njin/boot");
124
+ const { printBanner } = await import("@njinlabs/njin/banner");
125
+
126
+ await boot();
127
+ printBanner({ mode: "production" });
128
+ `,
129
+ );
130
+
131
+ const exeName = process.platform === "win32" ? "server.exe" : "server";
132
+
133
+ const result = await Bun.build({
134
+ entrypoints: [entryPath],
135
+ compile: { outfile: join(outDir, exeName) },
136
+ // vite is dev-only (view.ts gates it behind `if (isDev)`, dead in production) — bundling
137
+ // it would also drag in its own internal (not-installed) lazy `import("esbuild")`.
138
+ external: ["vite"],
139
+ plugins: [surrealNativePlugin],
140
+ // The CLI's `bun build --compile` implicitly inlines process.env.NODE_ENV as "production";
141
+ // the Bun.build() JS API doesn't, so view.ts's top-level `isDev` check would otherwise read
142
+ // undefined and take the dev branch (importing the externalized, not-on-disk `vite`).
143
+ define: {
144
+ "process.env.NODE_ENV": JSON.stringify("production"),
145
+ },
146
+ });
147
+
148
+ if (!result.success) {
149
+ console.error("\n✗ Build failed");
150
+ for (const message of result.logs) console.error(message);
151
+ process.exit(1);
152
+ }
153
+
154
+ console.log(`\n✓ Compiled server -> out/${exeName}`);
155
+ console.log(
156
+ "\nNote: out/bindings/ must ship alongside the executable — it holds the native SurrealDB " +
157
+ "binding loaded at runtime.",
158
+ );
159
+ console.log(`\nRun it with:\n cd out && ./${exeName}`);
160
+ } finally {
161
+ await rm(entryPath, { force: true });
162
+ }
package/src/cli/dev.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Static imports are hoisted and evaluated before this file's own body runs, so setting
2
+ // NODE_ENV here would be too late if "../core/boot" were a static import — view.ts
3
+ // (reached transitively) reads NODE_ENV at module-load time. Dynamic imports execute in
4
+ // normal program order, so they run after the line below.
5
+ process.env.NODE_ENV ??= "development";
6
+
7
+ const { boot } = await import("../core/boot");
8
+ const { printBanner } = await import("../core/banner");
9
+
10
+ await boot();
11
+ printBanner({ mode: "development" });
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bun
2
+ const [command] = process.argv.slice(2);
3
+
4
+ switch (command) {
5
+ case "dev":
6
+ await import("./dev");
7
+ break;
8
+ case "start":
9
+ await import("./start");
10
+ break;
11
+ case "build":
12
+ await import("./build");
13
+ break;
14
+ default:
15
+ console.log(`Usage: njin <dev|build|start>
16
+
17
+ dev Run the dev server (Vite HMR + live reload)
18
+ build Build for production -> ./out (public/, _admin/, views/, server)
19
+ start Run from source in production mode (no compile)`);
20
+ process.exit(command ? 1 : 0);
21
+ }
@@ -0,0 +1,9 @@
1
+ // See dev.ts for why these are dynamic imports — NODE_ENV must be set before
2
+ // view.ts (reached transitively) evaluates, and static imports would run first.
3
+ process.env.NODE_ENV = "production";
4
+
5
+ const { boot } = await import("../core/boot");
6
+ const { printBanner } = await import("../core/banner");
7
+
8
+ await boot();
9
+ printBanner({ mode: "production" });
@@ -0,0 +1,34 @@
1
+ import { loadConfig } from "../core/config";
2
+ import admin from "../modules/admin";
3
+ import analytics from "../modules/analytics";
4
+ import api from "../modules/api";
5
+ import auth from "../modules/auth";
6
+ import elysia from "../modules/elysia";
7
+ import file from "../modules/file";
8
+ import img from "../modules/img";
9
+ import logger from "../modules/logger";
10
+ import setup from "../modules/setup";
11
+ import surreal from "../modules/surreal";
12
+ import users from "../modules/users";
13
+ import view from "../modules/view";
14
+
15
+ // Must resolve before anything below reads getConfig() — db path, port, file
16
+ // adapter, and the model registry all come from the consuming project's config.ts.
17
+ await loadConfig();
18
+
19
+ const modules = [
20
+ logger.init(),
21
+ surreal.init(),
22
+ elysia.init(),
23
+ await file.init(),
24
+ await auth.init(),
25
+ await setup.init(),
26
+ await api.init(),
27
+ await img.init(),
28
+ await analytics.init(),
29
+ await users.init(),
30
+ await admin.init(),
31
+ await view.init(),
32
+ ] as const;
33
+
34
+ export default modules;
@@ -0,0 +1,37 @@
1
+ import type { FileAdapter } from "../../modules/file";
2
+ import { init } from "@paralleldrive/cuid2";
3
+ import { join } from "node:path";
4
+ import z from "zod";
5
+
6
+ const createId = init({
7
+ random: Math.random,
8
+ length: 10,
9
+ fingerprint: "file",
10
+ });
11
+
12
+ const meta = z.null();
13
+
14
+ const bunFilesystemAdapter = ({ dir = "./uploads" }: { dir?: string } = {}): FileAdapter<typeof meta> => {
15
+ return {
16
+ meta,
17
+ dir,
18
+ write: async (file) => {
19
+ const [fileName, ...exts] = file.name.split(".");
20
+
21
+ const name = `${fileName}_${createId()}.${exts.join(".")}`;
22
+
23
+ await Bun.write(join(dir, name), await file.arrayBuffer());
24
+
25
+ return {
26
+ meta: null,
27
+ name,
28
+ size: file.size,
29
+ type: file.type,
30
+ url: `/uploads/${name}`,
31
+ };
32
+ },
33
+ unlink: (file) => Bun.file(join(dir, file.name)).delete(),
34
+ };
35
+ };
36
+
37
+ export default bunFilesystemAdapter;
@@ -0,0 +1,60 @@
1
+ import type { FileAdapter } from "../../modules/file";
2
+ import { init } from "@paralleldrive/cuid2";
3
+ import type { S3Options } from "bun";
4
+ import z from "zod";
5
+
6
+ const createId = init({
7
+ random: Math.random,
8
+ length: 10,
9
+ fingerprint: "file",
10
+ });
11
+
12
+ const meta = z.null();
13
+
14
+ type ResolveUrlOptions = {
15
+ publicUrl?: string;
16
+ endpoint?: string;
17
+ bucket?: string;
18
+ region?: string;
19
+ };
20
+
21
+ // Path-style works for AWS S3 and S3-compatible services alike (R2, Spaces, MinIO).
22
+ // No process.env fallback here on purpose — the consuming project's config.ts is the
23
+ // single place that decides whether a value comes from an env var or is hardcoded.
24
+ export const resolveUrl = (key: string, options: ResolveUrlOptions = {}): string => {
25
+ if (options.publicUrl) return `${options.publicUrl.replace(/\/$/, "")}/${key}`;
26
+
27
+ if (options.endpoint) return `${options.endpoint.replace(/\/$/, "")}/${options.bucket}/${key}`;
28
+
29
+ const region = options.region ?? "us-east-1";
30
+ return `https://${options.bucket}.s3.${region}.amazonaws.com/${key}`;
31
+ };
32
+
33
+ // `bucket` is required (unlike Bun.S3Client itself, which would otherwise fall back to
34
+ // S3_BUCKET/AWS_BUCKET env vars) — resolveUrl() needs to know it regardless, and reading
35
+ // it here directly would bypass the project's own config.ts as the single source of truth.
36
+ const s3Adapter = (options: S3Options & { bucket: string; publicUrl?: string }): FileAdapter<typeof meta> => {
37
+ const { publicUrl, ...s3Options } = options;
38
+ const bucket = new Bun.S3Client(s3Options);
39
+
40
+ return {
41
+ meta,
42
+ write: async (file) => {
43
+ const [fileName, ...exts] = file.name.split(".");
44
+ const key = `${fileName}_${createId()}.${exts.join(".")}`;
45
+
46
+ await bucket.write(key, file, { acl: "public-read", type: file.type });
47
+
48
+ return {
49
+ meta: null,
50
+ name: key,
51
+ size: file.size,
52
+ type: file.type,
53
+ url: resolveUrl(key, { publicUrl, endpoint: s3Options.endpoint, bucket: s3Options.bucket, region: s3Options.region }),
54
+ };
55
+ },
56
+ unlink: (file) => bucket.delete(file.name),
57
+ };
58
+ };
59
+
60
+ export default s3Adapter;