nibula 1.1.0 → 1.1.2

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/CHANGELOG.md CHANGED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to Nibula are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2026-07-21
9
+
10
+ ### Added
11
+ - **Node.js backend** as an alternative to PHP, living side by side in
12
+ `src/backend/`: `_core/index.js` front controller (which is also the HTTP
13
+ server), `Response` and `RateLimiter` modules, a `mysql2` pool singleton
14
+ `Database.js`, `example.config.js`, and a `package.json` for the backend's
15
+ own dependencies. Same REST API as PHP: routing, `X-Api-Key` auth, CORS and
16
+ file-based rate limiting are identical.
17
+ - **Backend choice at scaffold time**: `nib new` now asks whether to use Node.js
18
+ or PHP, in addition to language and CSS framework.
19
+ - `backend/backend-node.service.example` — a ready-to-adapt **systemd** unit for
20
+ keeping the Node backend running on a server.
21
+ - `config.js` is generated from `example.config.js` at scaffold time (mirroring
22
+ the existing `config.php` generation).
23
+ - Deployment docs now cover running the Node backend with **`screen`** (quick /
24
+ small setups) and **systemd** (production), plus environment variables.
25
+
26
+ ### Changed
27
+ - **Composer is now required only when the PHP backend is chosen.** If you pick
28
+ Node, `composer install` is skipped entirely and the backend's npm
29
+ dependencies (`mysql2`) are installed instead.
30
+ - `.htaccess`, `web.config` and `nginx.conf` now cover **both** backends. On
31
+ nginx, `/api` goes to Node and automatically falls back to the PHP front
32
+ controller when Node is unreachable. On Apache/IIS the active backend is
33
+ selected by configuration (documented), since per-directory configs can't
34
+ health-check an upstream.
35
+ - `.gitignore` now also ignores `src/backend/config.js`,
36
+ `src/backend/node_modules/` and `src/backend/cache/`.
37
+ - Documentation (`README.md`, `docs/Backend.md`, `docs/Deploy.md`) updated:
38
+ backend examples now use JavaScript/Node as the reference, with the equivalent
39
+ PHP shown alongside and a note that PHP behaves identically.
40
+
41
+ ### Notes
42
+ - Both backends are always scaffolded, so you can switch later without
43
+ re-creating the project. The PHP front controller only serves `.php` endpoint
44
+ files and the Node one only `.js`, so they coexist without conflict.
45
+
46
+ ## [1.0.2]
47
+
48
+ - Previous release (PHP backend only).
49
+
50
+ [1.1.0]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.1.0
51
+ [1.0.2]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.0.2
package/README.md CHANGED
@@ -21,7 +21,7 @@ Building a website from scratch involves a lot of moving parts: templating, buil
21
21
  - ðŸŠķ **Lightweight by default** — SCSS frameworks can be filtered so you ship only what you actually use
22
22
  - 🌍 **Open source** — free to use, free to modify, free to share
23
23
 
24
- ![Version](https://img.shields.io/badge/version-1.1.0-blue)
24
+ ![Version](https://img.shields.io/badge/version-1.1.2-blue)
25
25
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
26
26
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
27
27
 
package/bin/create.js CHANGED
@@ -36,7 +36,7 @@ const BACKEND = Object.freeze({
36
36
  // ── CHOICES ──────────────────────────────────────────────────────────────────
37
37
 
38
38
  const LANGUAGE_CHOICES = [
39
- { label: 'JavaScript (recomened)', value: LANGUAGE.JAVASCRIPT },
39
+ { label: 'JavaScript (recomended)', value: LANGUAGE.JAVASCRIPT },
40
40
  { label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
41
41
  ];
42
42
 
@@ -53,6 +53,12 @@ const BACKEND_CHOICES = [
53
53
  { label: 'PHP (index.php — Composer dependencies)', value: BACKEND.PHP },
54
54
  ];
55
55
 
56
+ // Runtime dependency added to the ROOT package.json when the Node backend is
57
+ // chosen, so it lands in the root node_modules (never in src/backend).
58
+ const NODE_BACKEND_DEPENDENCIES = {
59
+ mysql2: '^3.11.0',
60
+ };
61
+
56
62
  // ── COPY CONFIG ───────────────────────────────────────────────────────────────
57
63
 
58
64
  const MANDATORY_COPY = [
@@ -72,6 +78,14 @@ const CREATE_DIRS = [
72
78
  'src/frontend/_routes',
73
79
  ];
74
80
 
81
+ // Backend files that belong to exactly one backend, matched by basename.
82
+ // Everything else (migrations, .htaccess, web.config, README, ...) is shared.
83
+ const NODE_ONLY_FILES = new Set(['package.json', 'backend-node.service.example']);
84
+ const PHP_ONLY_FILES = new Set(['composer.json', 'composer.lock']);
85
+ const PHP_ONLY_DIRS = new Set(['vendor']);
86
+ // Runtime artifacts that must never be copied from the template, either way.
87
+ const BACKEND_SKIP_DIRS = new Set(['node_modules', 'cache', '.git']);
88
+
75
89
  // ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
76
90
 
77
91
  const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
@@ -128,7 +142,6 @@ src/backend/_core/vendor/
128
142
  out/
129
143
  src/backend/config.php
130
144
  src/backend/config.js
131
- src/backend/node_modules/
132
145
  src/backend/cache/
133
146
  `;
134
147
 
@@ -198,6 +211,48 @@ function copyRecursive(src, dest, exclude = []) {
198
211
  }
199
212
  }
200
213
 
214
+ /**
215
+ * Decide whether a backend entry belongs to the chosen backend.
216
+ * Returns false for entries that must be SKIPPED.
217
+ */
218
+ function backendEntryKept(basename, isDir, backend) {
219
+ // Never copy runtime artifacts from the template.
220
+ if (isDir && BACKEND_SKIP_DIRS.has(basename)) return false;
221
+
222
+ if (backend === BACKEND.NODE) {
223
+ // Node project: drop every PHP artifact.
224
+ if (basename.endsWith('.php')) return false;
225
+ if (PHP_ONLY_FILES.has(basename)) return false;
226
+ if (isDir && PHP_ONLY_DIRS.has(basename)) return false;
227
+ return true;
228
+ }
229
+
230
+ // PHP project: drop every Node artifact.
231
+ if (basename.endsWith('.js')) return false;
232
+ if (NODE_ONLY_FILES.has(basename)) return false;
233
+ return true;
234
+ }
235
+
236
+ /**
237
+ * Copy src/backend into the project keeping only the chosen backend's files.
238
+ * Shared files (SQL migrations, .htaccess, web.config, README, ...) are kept
239
+ * for both. Empty directories left behind by filtering are not created.
240
+ */
241
+ function copyBackend(src, dest, backend) {
242
+ const stat = fs.statSync(src);
243
+ if (stat.isDirectory()) {
244
+ for (const child of fs.readdirSync(src)) {
245
+ const childSrc = path.join(src, child);
246
+ const isDir = fs.statSync(childSrc).isDirectory();
247
+ if (!backendEntryKept(child, isDir, backend)) continue;
248
+ copyBackend(childSrc, path.join(dest, child), backend);
249
+ }
250
+ } else {
251
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
252
+ fs.copyFileSync(src, dest);
253
+ }
254
+ }
255
+
201
256
  function slashComment(content, line) {
202
257
  content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
203
258
  return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
@@ -217,8 +272,7 @@ function njkUncomment(content, line) {
217
272
  }
218
273
 
219
274
  function installDependencies(backend) {
220
- const backendRoot = path.join(targetDir, 'src', 'backend');
221
- const backendCore = path.join(backendRoot, '_core');
275
+ const backendCore = path.join(targetDir, 'src', 'backend', '_core');
222
276
 
223
277
  log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
224
278
  const npm = spawnSync('npm', ['install'], {
@@ -234,17 +288,11 @@ function installDependencies(backend) {
234
288
  return false;
235
289
  }
236
290
 
237
- // --- Node backend: install its own deps (mysql2), then SKIP Composer ---
291
+ // Node backend: its runtime deps were added to the ROOT package.json and
292
+ // installed above into the root node_modules — there is no separate install
293
+ // inside src/backend, and Composer is never run.
238
294
  if (backend === BACKEND.NODE) {
239
- if (fs.existsSync(path.join(backendRoot, 'package.json'))) {
240
- log(`\n${color.blue}>> Installing Node backend modules...${color.reset}`);
241
- spawnSync('npm', ['install'], {
242
- cwd: backendRoot,
243
- stdio: 'inherit',
244
- shell: process.platform === 'win32',
245
- });
246
- }
247
- return true; // Composer is never run for the Node backend
295
+ return true;
248
296
  }
249
297
 
250
298
  // --- PHP backend: install Composer dependencies (if present) ---
@@ -389,11 +437,19 @@ async function init() {
389
437
  const src = path.join(templateDir, target);
390
438
  const dest = path.join(targetDir, target);
391
439
  if (!fs.existsSync(src)) continue;
392
- const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
393
- copyRecursive(src, dest, exclude);
440
+
441
+ if (target === 'src/backend') {
442
+ // Copy only the chosen backend's files (the other backend is omitted).
443
+ copyBackend(src, dest, backend);
444
+ } else {
445
+ const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
446
+ copyRecursive(src, dest, exclude);
447
+ }
394
448
  logAdd(target);
395
449
  }
396
450
 
451
+ // Generate the local config only for the chosen backend. The other backend's
452
+ // example file was not copied, so its guard below is simply skipped.
397
453
  const configDest = path.join(targetDir, 'src/backend/config.php');
398
454
  const configExample = path.join(targetDir, 'src/backend/example.config.php');
399
455
  if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
@@ -408,7 +464,17 @@ async function init() {
408
464
  logAdd('src/backend/config.js');
409
465
  }
410
466
 
467
+ // Build the project package.json. Clone the shared dependency maps so we
468
+ // never mutate the PROJECT_PACKAGE constant.
411
469
  const pkg = { ...PROJECT_PACKAGE };
470
+ pkg.dependencies = { ...PROJECT_PACKAGE.dependencies };
471
+ pkg.devDependencies = { ...PROJECT_PACKAGE.devDependencies };
472
+
473
+ // Node backend deps live in the ROOT node_modules — add them to root deps
474
+ // so `npm install` installs them there (never in src/backend).
475
+ if (backend === BACKEND.NODE) {
476
+ pkg.dependencies = { ...pkg.dependencies, ...NODE_BACKEND_DEPENDENCIES };
477
+ }
412
478
 
413
479
  if (language === LANGUAGE.TYPESCRIPT) {
414
480
  const tsSrc = path.join(templateDir, 'tsconfig.json');
package/docs/Backend.md CHANGED
@@ -27,43 +27,51 @@ When you run `nib new`, the scaffolder asks three questions: language, CSS
27
27
  framework, and **backend**. Pick Node.js or PHP.
28
28
 
29
29
  - **If you pick Node**, Composer is never run — you get an npm-only setup, and
30
- the backend's own dependency (`mysql2`, used only if you touch the database)
31
- is installed for you.
32
- - **If you pick PHP**, Composer dependencies are installed as before.
30
+ the backend's dependency (`mysql2`, used only if you touch the database) is
31
+ added to the **root** `package.json` and installed into the root `node_modules`.
32
+ - **If you pick PHP**, Composer dependencies are installed as before, and no Node
33
+ backend files are added.
33
34
 
34
- Both file sets are always present in `src/backend/`, so you can switch later
35
- without re-scaffolding. The PHP front controller only looks at `.php` endpoint
36
- files and the Node one only at `.js` files, so they coexist without conflict.
35
+ **Only the backend you choose is copied into the project.** A Node project
36
+ contains no PHP files; a PHP project contains no `.js` backend files. Shared,
37
+ backend-agnostic files (SQL migrations, `.htaccess`, `web.config`) are always
38
+ included.
37
39
 
38
40
  ## Structure
39
41
 
42
+ The structure below shows both backends for reference; **your project will have
43
+ only one side of each pair** (plus the shared files).
44
+
40
45
  ```
41
46
  src/backend/
42
47
  ├── api/
43
48
  │ ├── public/ # Endpoints accessible without an API key
44
49
  │ └── protected/ # Endpoints requiring the X-Api-Key header
45
50
  ├── _core/ # Framework internals (routing, modules) — do not edit
46
- │ ├── index.php # PHP front controller
47
- │ ├── index.js # Node front controller (also the HTTP server)
51
+ │ ├── index.php # PHP front controller (PHP project)
52
+ │ ├── index.js # Node front controller + server (Node project)
48
53
  │ ├── init.php / init.js
49
- │ └── modules/ # Response, RateLimiter (.php and .js)
54
+ │ └── modules/ # Response, RateLimiter
50
55
  ├── database/
51
- │ ├── Database.php # PDO singleton
52
- │ ├── Database.js # mysql2 pool singleton
53
- │ └── migrations/
54
- ├── package.json # Node backend deps + start script
55
- ├── example.config.php # PHP template — versioned, safe to commit
56
- ├── example.config.js # Node template — versioned, safe to commit
57
- ├── config.php # Local PHP config — generated on setup, never commit
58
- └── config.js # Local Node config — generated on setup, never commit
56
+ │ ├── Database.php # PDO singleton (PHP project)
57
+ │ ├── Database.js # mysql2 pool singleton (Node project)
58
+ │ └── migrations/ # shared
59
+ ├── package.json # Node project only — backend deps + start script
60
+ ├── example.config.php # PHP project only — versioned template
61
+ ├── example.config.js # Node project only — versioned template
62
+ ├── config.php # PHP project only — generated on setup, never commit
63
+ └── config.js # Node project only — generated on setup, never commit
59
64
  ```
60
65
 
61
- > A scaffolded project contains **both** the example and the generated config for
62
- > each backend. `config.php` / `config.js` are generated automatically by
63
- > `nib new` (copies of the examples) and are git-ignored, so your secrets stay
64
- > local. The `example.config.*` files are versioned: they end up on GitHub as
65
- > secret-free references. If a `config.*` is ever missing (e.g. after a fresh
66
- > clone), just copy the matching `example.config.*` to it.
66
+ > `config.php` / `config.js` are generated automatically by `nib new` (a copy of
67
+ > the matching `example.config.*`) and are git-ignored, so your secrets stay
68
+ > local. The `example.config.*` file is versioned as a secret-free reference. If
69
+ > `config.*` is ever missing (e.g. after a fresh clone), copy the example to it.
70
+
71
+ > **Where do the Node backend's dependencies live?** In the **root**
72
+ > `node_modules`, not in `src/backend` — so your source tree stays clean (no
73
+ > `src/backend/node_modules` locally). On the server you install them into
74
+ > `out/backend` instead; see `docs/Deploy.md`.
67
75
 
68
76
  ## Key difference: how each backend runs
69
77
 
package/nginx.conf CHANGED
@@ -1,75 +1,75 @@
1
- # Example nginx directives for a Nibula site — covers BOTH backends.
2
- #
3
- # /api is served by Node (index.js) if it is running on 127.0.0.1:3000; if Node
4
- # is unreachable, nginx falls back to the PHP front controller automatically.
5
- # So the same config works whether you deployed the Node backend or the PHP one:
6
- # - Node backend running -> Node answers /api
7
- # - only PHP deployed -> Node is down -> falls back to PHP-FPM
8
- #
9
- # This is NOT a ready-to-use file: paste these directives inside your own
10
- # server { } block (the one with server_name, listen and SSL) and set `root`
11
- # to your `out` folder.
12
-
13
- # Hide the nginx version from response headers
14
- server_tokens off;
15
-
16
- # Force HTTPS on future requests (applies site-wide, including /api)
17
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
18
-
19
- root /var/www/your-site/out;
20
- index index.html;
21
-
22
- # Disable directory listing
23
- autoindex off;
24
-
25
- error_page 403 404 /404.html;
26
- location = /404.html {
27
- internal;
28
- }
29
-
30
- # Never expose the backend source directory over HTTP.
31
- location ^~ /backend/ {
32
- return 404;
33
- }
34
-
35
- # Block server config files anywhere in the tree (.htaccess, web.config).
36
- location ~* (^|/)(\.htaccess|web\.config)$ {
37
- return 404;
38
- }
39
-
40
- # --- /api: Node primary, PHP fallback ---------------------------------------
41
- # Try the Node backend first. If it is down (502/504), retry through the PHP
42
- # front controller. Security headers for API responses are set by the backend.
43
- location ^~ /api/ {
44
- proxy_pass http://127.0.0.1:3000;
45
- proxy_http_version 1.1;
46
- proxy_set_header Host $host;
47
- proxy_set_header X-Real-IP $remote_addr;
48
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49
- proxy_set_header X-Forwarded-Proto $scheme;
50
-
51
- proxy_intercept_errors on;
52
- error_page 502 504 = @php_backend;
53
- }
54
-
55
- location @php_backend {
56
- include fastcgi_params;
57
- fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
58
-
59
- # Adjust the PHP-FPM socket to your distro:
60
- # Debian / Ubuntu : unix:/run/php/php8.3-fpm.sock
61
- # RHEL / Fedora : unix:/run/php-fpm/php-fpm.sock
62
- # TCP fallback : 127.0.0.1:9000
63
- fastcgi_pass unix:/run/php/php8.3-fpm.sock;
64
- }
65
-
66
- # Serve static files, fall back to the 404 page.
67
- # A location with add_header does not inherit server-level headers,
68
- # so Strict-Transport-Security is repeated here.
69
- location / {
70
- add_header X-Content-Type-Options "nosniff" always;
71
- add_header X-Frame-Options "SAMEORIGIN" always;
72
- add_header Referrer-Policy "strict-origin-when-cross-origin" always;
73
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
74
- try_files $uri $uri/ /404.html;
75
- }
1
+ # Example nginx directives for a Nibula site — covers BOTH backends.
2
+ #
3
+ # /api is served by Node (index.js) if it is running on 127.0.0.1:3000; if Node
4
+ # is unreachable, nginx falls back to the PHP front controller automatically.
5
+ # So the same config works whether you deployed the Node backend or the PHP one:
6
+ # - Node backend running -> Node answers /api
7
+ # - only PHP deployed -> Node is down -> falls back to PHP-FPM
8
+ #
9
+ # This is NOT a ready-to-use file: paste these directives inside your own
10
+ # server { } block (the one with server_name, listen and SSL) and set `root`
11
+ # to your `out` folder.
12
+
13
+ # Hide the nginx version from response headers
14
+ server_tokens off;
15
+
16
+ # Force HTTPS on future requests (applies site-wide, including /api)
17
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
18
+
19
+ root /var/www/your-site/out;
20
+ index index.html;
21
+
22
+ # Disable directory listing
23
+ autoindex off;
24
+
25
+ error_page 403 404 /404.html;
26
+ location = /404.html {
27
+ internal;
28
+ }
29
+
30
+ # Never expose the backend source directory over HTTP.
31
+ location ^~ /backend/ {
32
+ return 404;
33
+ }
34
+
35
+ # Block server config files anywhere in the tree (.htaccess, web.config).
36
+ location ~* (^|/)(\.htaccess|web\.config)$ {
37
+ return 404;
38
+ }
39
+
40
+ # --- /api: Node primary, PHP fallback ---------------------------------------
41
+ # Try the Node backend first. If it is down (502/504), retry through the PHP
42
+ # front controller. Security headers for API responses are set by the backend.
43
+ location ^~ /api/ {
44
+ proxy_pass http://127.0.0.1:3000;
45
+ proxy_http_version 1.1;
46
+ proxy_set_header Host $host;
47
+ proxy_set_header X-Real-IP $remote_addr;
48
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49
+ proxy_set_header X-Forwarded-Proto $scheme;
50
+
51
+ proxy_intercept_errors on;
52
+ error_page 502 504 = @php_backend;
53
+ }
54
+
55
+ location @php_backend {
56
+ include fastcgi_params;
57
+ fastcgi_param SCRIPT_FILENAME $document_root/backend/_core/index.php;
58
+
59
+ # Adjust the PHP-FPM socket to your distro:
60
+ # Debian / Ubuntu : unix:/run/php/php8.3-fpm.sock
61
+ # RHEL / Fedora : unix:/run/php-fpm/php-fpm.sock
62
+ # TCP fallback : 127.0.0.1:9000
63
+ fastcgi_pass unix:/run/php/php8.3-fpm.sock;
64
+ }
65
+
66
+ # Serve static files, fall back to the 404 page.
67
+ # A location with add_header does not inherit server-level headers,
68
+ # so Strict-Transport-Security is repeated here.
69
+ location / {
70
+ add_header X-Content-Type-Options "nosniff" always;
71
+ add_header X-Frame-Options "SAMEORIGIN" always;
72
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
73
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
74
+ try_files $uri $uri/ /404.html;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A beginner-friendly static site generator built on Eleventy that stays close to vanilla HTML, CSS and JS — with a page-management CLI, an optional PHP backend, and SEO files generated for you.",
5
5
  "keywords": [
6
6
  "static-site-generator",