@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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/package.json +59 -0
- package/src/cli/build.ts +162 -0
- package/src/cli/dev.ts +11 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/start.ts +9 -0
- package/src/config/module.ts +34 -0
- package/src/core/adapters/bun_filesystem.ts +37 -0
- package/src/core/adapters/s3.ts +60 -0
- package/src/core/banner.ts +54 -0
- package/src/core/boot.ts +10 -0
- package/src/core/config.ts +89 -0
- package/src/core/http_error.ts +21 -0
- package/src/core/model/data_type/array.ts +22 -0
- package/src/core/model/data_type/boolean.ts +15 -0
- package/src/core/model/data_type/date.ts +28 -0
- package/src/core/model/data_type/email.ts +21 -0
- package/src/core/model/data_type/file.ts +15 -0
- package/src/core/model/data_type/index.ts +13 -0
- package/src/core/model/data_type/multi_file.ts +18 -0
- package/src/core/model/data_type/numeric.ts +15 -0
- package/src/core/model/data_type/object.ts +15 -0
- package/src/core/model/data_type/relation.ts +73 -0
- package/src/core/model/data_type/relation_many.ts +55 -0
- package/src/core/model/data_type/richtext.ts +20 -0
- package/src/core/model/data_type/select.ts +15 -0
- package/src/core/model/data_type/text.ts +20 -0
- package/src/core/model/index.ts +244 -0
- package/src/core/module/index.ts +12 -0
- package/src/core/path_guard.ts +9 -0
- package/src/models/file.ts +17 -0
- package/src/models/user.ts +18 -0
- package/src/modules/admin.ts +43 -0
- package/src/modules/analytics.ts +212 -0
- package/src/modules/api.ts +160 -0
- package/src/modules/auth.ts +153 -0
- package/src/modules/elysia.ts +85 -0
- package/src/modules/file.ts +131 -0
- package/src/modules/img.ts +137 -0
- package/src/modules/logger.ts +18 -0
- package/src/modules/setup.ts +89 -0
- package/src/modules/surreal.ts +58 -0
- package/src/modules/users.ts +94 -0
- package/src/modules/view.ts +247 -0
- 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
|
+
}
|
package/src/cli/build.ts
ADDED
|
@@ -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" });
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/start.ts
ADDED
|
@@ -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;
|