@njinlabs/njin 0.2.0 → 0.3.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 CHANGED
@@ -131,6 +131,37 @@ This automatically generates:
131
131
  <title>{{ settings.siteName }}</title>
132
132
  ```
133
133
 
134
+ ## Helpers — custom template functions
135
+
136
+ `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.
137
+
138
+ ```ts
139
+ // src/helpers/format_date.ts
140
+ import { defineHelper } from "@njinlabs/njin";
141
+ import moment from "moment";
142
+
143
+ export default defineHelper("formatDate", (date: string, format = "DD MMM YYYY") =>
144
+ moment(date).format(format),
145
+ );
146
+ ```
147
+
148
+ Register it in `config.ts`:
149
+
150
+ ```ts
151
+ // config.ts
152
+ export default defineConfig({
153
+ helpers: [
154
+ () => import("./src/helpers/format_date"),
155
+ ],
156
+ });
157
+ ```
158
+
159
+ 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):
160
+
161
+ ```edge
162
+ <p>{{ formatDate(post.createdAt) }}</p>
163
+ ```
164
+
134
165
  ## Events
135
166
 
136
167
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@njinlabs/njin",
3
- "version": "0.2.0",
3
+ "version": "0.3.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"],
@@ -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 });
@@ -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";
@@ -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[];
@@ -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) {