@njinlabs/njin 0.3.0 → 0.5.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
@@ -1,5 +1,10 @@
1
1
  # njin
2
2
 
3
+ [![CI](https://github.com/njinlabs/njin/actions/workflows/ci.yml/badge.svg)](https://github.com/njinlabs/njin/actions/workflows/ci.yml)
4
+ [![CodeQL](https://github.com/njinlabs/njin/actions/workflows/codeql.yml/badge.svg)](https://github.com/njinlabs/njin/actions/workflows/codeql.yml)
5
+ [![npm version](https://img.shields.io/npm/v/%40njinlabs%2Fnjin.svg)](https://www.npmjs.com/package/@njinlabs/njin)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
- bun create njin-app my-web
21
+ bunx @njinlabs/njin create my-web
17
22
  cd my-web
18
23
  bunx njin dev
19
24
  ```
@@ -389,9 +394,11 @@ adapters: {
389
394
  ## Commands
390
395
 
391
396
  ```bash
397
+ bunx @njinlabs/njin create my-web # Scaffold a new project
392
398
  bunx njin dev # Start dev server (Elysia + Vite HMR)
393
399
  bunx njin build # Build for production -> ./out (public/, _admin/, views/, compiled server)
394
400
  bunx njin start # Run from source in production mode (no compile)
401
+ bunx njin update # Update @njinlabs/njin to the latest version and refresh _admin/
395
402
  ```
396
403
 
397
404
  Wire these as `package.json` scripts (`"dev": "njin dev"`, etc.) if you'd rather run `bun run dev`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@njinlabs/njin",
3
- "version": "0.3.0",
3
+ "version": "0.5.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", "-f", "-", "--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;
@@ -11,11 +14,16 @@ switch (command) {
11
14
  case "build":
12
15
  await import("./build");
13
16
  break;
17
+ case "update":
18
+ await import("./update");
19
+ break;
14
20
  default:
15
- console.log(`Usage: njin <dev|build|start>
21
+ console.log(`Usage: njin <create|dev|build|start|update>
16
22
 
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)`);
23
+ create <dir> Scaffold a new project (defaults to current directory)
24
+ dev Run the dev server (Vite HMR + live reload)
25
+ build Build for production -> ./out (public/, _admin/, views/, server)
26
+ start Run from source in production mode (no compile)
27
+ update Update @njinlabs/njin to the latest version and refresh _admin/`);
20
28
  process.exit(command ? 1 : 0);
21
29
  }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+
6
+ const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m" };
7
+
8
+ const root = process.cwd();
9
+ const pkgPath = join(root, "package.json");
10
+
11
+ if (!existsSync(pkgPath)) {
12
+ console.error(`✖ No package.json found in ${root} — run this from an njin project root.`);
13
+ process.exit(1);
14
+ }
15
+
16
+ const pkg = await Bun.file(pkgPath).json();
17
+ const depKey = pkg.dependencies?.["@njinlabs/njin"]
18
+ ? "dependencies"
19
+ : pkg.devDependencies?.["@njinlabs/njin"]
20
+ ? "devDependencies"
21
+ : undefined;
22
+
23
+ if (!depKey) {
24
+ console.error(`✖ "@njinlabs/njin" is not listed in this project's package.json — run this from an njin project root.`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const currentPinned = String(pkg[depKey]["@njinlabs/njin"]).replace(/^[\^~]/, "");
29
+
30
+ console.log("Checking latest njin version...\n");
31
+
32
+ const registryRes = await fetch("https://registry.npmjs.org/@njinlabs/njin/latest");
33
+ if (!registryRes.ok) {
34
+ console.error(`✖ Could not reach the npm registry (HTTP ${registryRes.status}).`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const { version: latest } = (await registryRes.json()) as { version: string };
39
+ const adminDir = join(root, "_admin");
40
+
41
+ if (currentPinned === latest && existsSync(adminDir)) {
42
+ console.log(`${c.green}✓${c.reset} Already on the latest version (${latest}) — nothing to update.`);
43
+ process.exit(0);
44
+ }
45
+
46
+ console.log(`Updating njin ${currentPinned} -> ${latest}...\n`);
47
+
48
+ pkg[depKey]["@njinlabs/njin"] = `^${latest}`;
49
+ await Bun.write(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
50
+
51
+ console.log("Installing dependencies...\n");
52
+
53
+ const install = Bun.spawn(["bun", "install"], { cwd: root, stdout: "inherit", stderr: "inherit" });
54
+ const installExitCode = await install.exited;
55
+
56
+ if (installExitCode !== 0) {
57
+ console.error(`\n✖ "bun install" failed (exit code ${installExitCode}).`);
58
+ console.error(` package.json now points at njin ${latest}; run "bun install" manually to retry.`);
59
+ process.exit(installExitCode);
60
+ }
61
+
62
+ console.log("\nSyncing admin panel...\n");
63
+
64
+ // _admin/ is a prebuilt SPA baked into the tagged release tarball, not something published to
65
+ // npm (package.json's "files" excludes template/) — so bumping the dependency above never
66
+ // touches it. It's rebuilt with content-hashed asset filenames on every release, so the old
67
+ // directory is removed first rather than merged, to avoid accumulating stale hashed files.
68
+ const tag = `v${latest}`;
69
+ const tarballUrl = `https://codeload.github.com/njinlabs/njin/tar.gz/refs/tags/${tag}`;
70
+
71
+ const res = await fetch(tarballUrl);
72
+ if (!res.ok || !res.body) {
73
+ console.error(
74
+ `✖ Could not download the admin panel for njin ${tag} (HTTP ${res.status}).\n` +
75
+ ` The dependency was updated to ${latest}, but _admin/ was not refreshed. Re-run "bunx njin update" to retry.`,
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ await rm(adminDir, { recursive: true, force: true });
81
+ await mkdir(adminDir, { recursive: true });
82
+
83
+ // --strip-components=3 drops the `njin-<version>/template/_admin/` prefix baked into GitHub's
84
+ // tag-tarball layout, so _admin/'s own contents land directly in adminDir.
85
+ const tar = Bun.spawn(["tar", "-xz", "-f", "-", "--strip-components=3", "-C", adminDir, `njin-${latest}/template/_admin`], {
86
+ stdin: res.body,
87
+ stdout: "inherit",
88
+ stderr: "inherit",
89
+ });
90
+
91
+ const tarExitCode = await tar.exited;
92
+
93
+ if (tarExitCode !== 0) {
94
+ console.error(
95
+ `✖ Failed to extract the admin panel (tar exited with code ${tarExitCode}).\n` +
96
+ ` The dependency was updated to ${latest}, but _admin/ was not refreshed.`,
97
+ );
98
+ process.exit(tarExitCode);
99
+ }
100
+
101
+ console.log(`
102
+ ${c.bold}${c.cyan}njin update${c.reset} ${c.dim}done${c.reset}
103
+
104
+ ${c.green}➜${c.reset} njin ${currentPinned} -> ${latest}
105
+ ${c.green}➜${c.reset} _admin/ refreshed
106
+ `);
@@ -62,10 +62,18 @@ const surreal = makeModule(() => {
62
62
  db = new Surreal({ engines });
63
63
  await db.connect(dbConfig.path, { authentication: dbConfig.auth });
64
64
  await db.use({ namespace: dbConfig.namespace, database: dbConfig.database });
65
- await ensureTables(db);
66
65
 
67
66
  return {
68
- spin: () => {
67
+ // Deferred to spin(), not run here — ensureTables() imports every registered model
68
+ // to read its prefix, which forces evaluation of data types like file()/multiFile()
69
+ // that depend on other modules' singletons (e.g. fileModule().model, only set once
70
+ // file.init() has run). Every module's init() — file's included — has already
71
+ // completed by the time any module's spin() starts, so this ordering is safe. It
72
+ // still runs before elysia's spin() (which calls app.listen()), since surreal is
73
+ // earlier in src/config/module.ts's array, so tables exist before the first request.
74
+ spin: async () => {
75
+ await ensureTables(db);
76
+
69
77
  const shutdown = async () => {
70
78
  await db.close();
71
79
  process.exit(0);