@njinlabs/njin 0.2.0 → 0.4.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/README.md +38 -1
- package/package.json +1 -1
- package/src/cli/create.ts +82 -0
- package/src/cli/index.ts +8 -4
- package/src/core/config.ts +9 -0
- package/src/core/helper.ts +5 -0
- package/src/core/model/index.ts +1 -0
- package/src/core/plugin.ts +2 -1
- package/src/modules/view.ts +5 -0
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# njin
|
|
2
2
|
|
|
3
|
+
[](https://github.com/njinlabs/njin/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/njinlabs/njin/actions/workflows/codeql.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@njinlabs/njin)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
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
9
|
|
|
5
10
|
## Stack
|
|
@@ -13,7 +18,7 @@ A modern framework for building company profiles, landing pages, and content-dri
|
|
|
13
18
|
## Quick start
|
|
14
19
|
|
|
15
20
|
```bash
|
|
16
|
-
|
|
21
|
+
bunx @njinlabs/njin create my-web
|
|
17
22
|
cd my-web
|
|
18
23
|
bunx njin dev
|
|
19
24
|
```
|
|
@@ -131,6 +136,37 @@ This automatically generates:
|
|
|
131
136
|
<title>{{ settings.siteName }}</title>
|
|
132
137
|
```
|
|
133
138
|
|
|
139
|
+
## Helpers — custom template functions
|
|
140
|
+
|
|
141
|
+
`helpers` register a plain, stateless function as an Edge global — unlike `vars`/`models`, there's no DB record and no auto-generated REST endpoint, just a function callable from any `.edge` template.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// src/helpers/format_date.ts
|
|
145
|
+
import { defineHelper } from "@njinlabs/njin";
|
|
146
|
+
import moment from "moment";
|
|
147
|
+
|
|
148
|
+
export default defineHelper("formatDate", (date: string, format = "DD MMM YYYY") =>
|
|
149
|
+
moment(date).format(format),
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Register it in `config.ts`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// config.ts
|
|
157
|
+
export default defineConfig({
|
|
158
|
+
helpers: [
|
|
159
|
+
() => import("./src/helpers/format_date"),
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use it directly in a template, no `await` needed since it's a plain synchronous function (an `async` `fn` works too, called with `await` like any other async global):
|
|
165
|
+
|
|
166
|
+
```edge
|
|
167
|
+
<p>{{ formatDate(post.createdAt) }}</p>
|
|
168
|
+
```
|
|
169
|
+
|
|
134
170
|
## Events
|
|
135
171
|
|
|
136
172
|
A type-safe event bus for fan-out notifications (e.g. "an order was paid", "a user registered") — different from model hooks (`beforeCreate`/`afterCreate`/...): hooks are scoped to one model and can abort the operation by throwing, while events are fire-and-forget — a listener that throws is logged but never stops other listeners or the code that dispatched.
|
|
@@ -358,6 +394,7 @@ adapters: {
|
|
|
358
394
|
## Commands
|
|
359
395
|
|
|
360
396
|
```bash
|
|
397
|
+
bunx @njinlabs/njin create my-web # Scaffold a new project
|
|
361
398
|
bunx njin dev # Start dev server (Elysia + Vite HMR)
|
|
362
399
|
bunx njin build # Build for production -> ./out (public/, _admin/, views/, compiled server)
|
|
363
400
|
bunx njin start # Run from source in production mode (no compile)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@njinlabs/njin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A modern framework for building company profiles, landing pages, and content-driven websites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["bun", "elysia", "surrealdb", "edgejs", "cms", "framework"],
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m" };
|
|
5
|
+
|
|
6
|
+
const targetArg = process.argv[3] ?? ".";
|
|
7
|
+
const targetDir = resolve(process.cwd(), targetArg);
|
|
8
|
+
const projectName = targetArg === "." ? "njin-app" : targetArg.split(/[\\/]/).filter(Boolean).pop()!;
|
|
9
|
+
|
|
10
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
11
|
+
console.error(`✖ Directory "${targetArg}" already exists and is not empty.`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// This CLI's own package.json — read at runtime (not a static JSON import) so it
|
|
16
|
+
// resolves correctly whether njin is a project-local dependency or fetched fresh
|
|
17
|
+
// by `bunx`. Doubles as the source of truth for which tagged template to fetch,
|
|
18
|
+
// and for the version pin written into the scaffolded project below.
|
|
19
|
+
const pkg = await Bun.file(join(import.meta.dir, "../../package.json")).json();
|
|
20
|
+
const tag = `v${pkg.version}`;
|
|
21
|
+
const tarballUrl = `https://codeload.github.com/njinlabs/njin/tar.gz/refs/tags/${tag}`;
|
|
22
|
+
|
|
23
|
+
console.log(`\nFetching template for njin ${tag}...\n`);
|
|
24
|
+
|
|
25
|
+
const res = await fetch(tarballUrl);
|
|
26
|
+
if (!res.ok || !res.body) {
|
|
27
|
+
console.error(
|
|
28
|
+
`✖ Could not download the project template for njin ${tag} (HTTP ${res.status}).\n` +
|
|
29
|
+
` Check your internet connection, or that tag "${tag}" exists at github.com/njinlabs/njin.`,
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mkdirSync(targetDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// `--strip-components=2` drops the `njin-<version>/template/` prefix baked into
|
|
37
|
+
// GitHub's tag-tarball layout, so template/ contents land directly in targetDir.
|
|
38
|
+
const tar = Bun.spawn(["tar", "-xz", "--strip-components=2", "-C", targetDir, `njin-${pkg.version}/template`], {
|
|
39
|
+
stdin: res.body,
|
|
40
|
+
stdout: "inherit",
|
|
41
|
+
stderr: "inherit",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const tarExitCode = await tar.exited;
|
|
45
|
+
|
|
46
|
+
if (tarExitCode !== 0) {
|
|
47
|
+
console.error(
|
|
48
|
+
`✖ Failed to extract the template (tar exited with code ${tarExitCode}).\n` +
|
|
49
|
+
` Make sure "tar" is installed and available on your PATH.`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(tarExitCode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pkgPath = join(targetDir, "package.json");
|
|
55
|
+
const scaffoldedPkg = await Bun.file(pkgPath).json();
|
|
56
|
+
scaffoldedPkg.name = projectName;
|
|
57
|
+
scaffoldedPkg.dependencies = { "@njinlabs/njin": `^${pkg.version}`, ...scaffoldedPkg.dependencies };
|
|
58
|
+
await Bun.write(pkgPath, JSON.stringify(scaffoldedPkg, null, 2) + "\n");
|
|
59
|
+
|
|
60
|
+
console.log("Installing dependencies...\n");
|
|
61
|
+
|
|
62
|
+
const install = Bun.spawn(["bun", "install"], {
|
|
63
|
+
cwd: targetDir,
|
|
64
|
+
stdout: "inherit",
|
|
65
|
+
stderr: "inherit",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const installExitCode = await install.exited;
|
|
69
|
+
|
|
70
|
+
if (installExitCode !== 0) {
|
|
71
|
+
console.error(`\n✖ "bun install" failed (exit code ${installExitCode}).`);
|
|
72
|
+
console.error(` The project was scaffolded at ${targetDir}, but dependencies are not installed.`);
|
|
73
|
+
console.error(` Run "cd ${targetArg} && bun install" manually to retry.\n`);
|
|
74
|
+
process.exit(installExitCode);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`
|
|
78
|
+
${c.bold}${c.cyan}njin create${c.reset} ${c.dim}done${c.reset}
|
|
79
|
+
|
|
80
|
+
${c.green}➜${c.reset} cd ${targetArg === "." ? "." : targetArg}
|
|
81
|
+
${c.green}➜${c.reset} bunx njin dev
|
|
82
|
+
`);
|
package/src/cli/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
const [command] = process.argv.slice(2);
|
|
3
3
|
|
|
4
4
|
switch (command) {
|
|
5
|
+
case "create":
|
|
6
|
+
await import("./create");
|
|
7
|
+
break;
|
|
5
8
|
case "dev":
|
|
6
9
|
await import("./dev");
|
|
7
10
|
break;
|
|
@@ -12,10 +15,11 @@ switch (command) {
|
|
|
12
15
|
await import("./build");
|
|
13
16
|
break;
|
|
14
17
|
default:
|
|
15
|
-
console.log(`Usage: njin <dev|build|start>
|
|
18
|
+
console.log(`Usage: njin <create|dev|build|start>
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
create <dir> Scaffold a new project (defaults to current directory)
|
|
21
|
+
dev Run the dev server (Vite HMR + live reload)
|
|
22
|
+
build Build for production -> ./out (public/, _admin/, views/, server)
|
|
23
|
+
start Run from source in production mode (no compile)`);
|
|
20
24
|
process.exit(command ? 1 : 0);
|
|
21
25
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AnyElysia } from "elysia";
|
|
|
2
2
|
import type { FileAdapter } from "../modules/file";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import bunFilesystemAdapter from "./adapters/bun_filesystem";
|
|
5
|
+
import type { Helper } from "./helper";
|
|
5
6
|
import type { makeModel } from "./model";
|
|
6
7
|
import type { Plugin } from "./plugin";
|
|
7
8
|
import type { makeVars } from "./vars";
|
|
@@ -28,6 +29,11 @@ export type RouteFactory = () => Promise<{ default: AnyElysia }>;
|
|
|
28
29
|
// a singleton settings object, not a list of records like a model.
|
|
29
30
|
export type VarsFactory = () => Promise<{ default: ReturnType<typeof makeVars> }>;
|
|
30
31
|
|
|
32
|
+
// A helper file's default export is one defineHelper() — a stateless function
|
|
33
|
+
// registered as an Edge global by its own name, unlike models/vars which are
|
|
34
|
+
// DB-backed objects registered by prefix.
|
|
35
|
+
export type HelperFactory = () => Promise<{ default: Helper }>;
|
|
36
|
+
|
|
31
37
|
export type NjinConfig = {
|
|
32
38
|
port?: number;
|
|
33
39
|
db?: {
|
|
@@ -50,6 +56,7 @@ export type NjinConfig = {
|
|
|
50
56
|
events?: EventFactory[];
|
|
51
57
|
routes?: RouteFactory[];
|
|
52
58
|
vars?: VarsFactory[];
|
|
59
|
+
helpers?: HelperFactory[];
|
|
53
60
|
plugins?: Plugin[];
|
|
54
61
|
};
|
|
55
62
|
|
|
@@ -64,6 +71,7 @@ export type ResolvedConfig = {
|
|
|
64
71
|
events: EventFactory[];
|
|
65
72
|
routes: RouteFactory[];
|
|
66
73
|
vars: VarsFactory[];
|
|
74
|
+
helpers: HelperFactory[];
|
|
67
75
|
// Everything else a Plugin contributes (models/vars/hooks/events/routes) is already
|
|
68
76
|
// flattened into the arrays above — init() is the only part that can't be merged away,
|
|
69
77
|
// so it's the only piece of each Plugin that survives resolution on its own.
|
|
@@ -131,6 +139,7 @@ export const loadConfig = async (preloaded?: NjinConfig): Promise<void> => {
|
|
|
131
139
|
events: [...plugins.flatMap((p) => p.events ?? []), ...(userConfig.events ?? [])],
|
|
132
140
|
routes: [...plugins.flatMap((p) => p.routes ?? []), ...(userConfig.routes ?? [])],
|
|
133
141
|
vars: [...plugins.flatMap((p) => p.vars ?? []), ...(userConfig.vars ?? [])],
|
|
142
|
+
helpers: [...plugins.flatMap((p) => p.helpers ?? []), ...(userConfig.helpers ?? [])],
|
|
134
143
|
pluginInits: plugins.map((p) => p.init).filter((fn): fn is () => Promise<void> | void => typeof fn === "function"),
|
|
135
144
|
};
|
|
136
145
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export type Helper = { name: string; fn: (...args: any[]) => unknown };
|
|
3
|
+
|
|
4
|
+
// Identity function — DX/typing only, same purpose as defineConfig/definePlugin.
|
|
5
|
+
export const defineHelper = (name: string, fn: Helper["fn"]): Helper => ({ name, fn });
|
package/src/core/model/index.ts
CHANGED
|
@@ -272,6 +272,7 @@ export const makeModel = <Rules extends z.ZodObject>(
|
|
|
272
272
|
export * from "./data_type";
|
|
273
273
|
export * from "./hooks";
|
|
274
274
|
export * from "../event";
|
|
275
|
+
export * from "../helper";
|
|
275
276
|
export * from "../plugin";
|
|
276
277
|
export * from "../route";
|
|
277
278
|
export * from "../vars";
|
package/src/core/plugin.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { EventFactory, HookFactory, ModelFactory, RouteFactory, VarsFactory } from "./config";
|
|
1
|
+
import type { EventFactory, HelperFactory, HookFactory, ModelFactory, RouteFactory, VarsFactory } from "./config";
|
|
2
2
|
|
|
3
3
|
export type Plugin = {
|
|
4
4
|
models?: ModelFactory[];
|
|
5
5
|
vars?: VarsFactory[];
|
|
6
|
+
helpers?: HelperFactory[];
|
|
6
7
|
hooks?: HookFactory[];
|
|
7
8
|
events?: EventFactory[];
|
|
8
9
|
routes?: RouteFactory[];
|
package/src/modules/view.ts
CHANGED
|
@@ -102,6 +102,11 @@ const view = makeModule(() => {
|
|
|
102
102
|
edge.global(group.prefix, group);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
for (const helperPromise of getConfig().helpers) {
|
|
106
|
+
const { default: helper } = await helperPromise();
|
|
107
|
+
edge.global(helper.name, helper.fn);
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
const controller = new Elysia();
|
|
106
111
|
|
|
107
112
|
if (!isDev) {
|