webspresso 0.0.66 → 0.0.67

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
@@ -17,6 +17,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
17
17
  - **Plugin System**: Extensible architecture with version control and inter-plugin communication
18
18
  - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint, optional REST CRUD routes from ORM models, optional admin UI for ORM query cache metrics and purge
19
19
  - **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for `express-session`, `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
20
+ - **Optional client runtime** (Alpine.js + [swup](https://swup.js.org/)): **`createApp({ clientRuntime: { alpine, swup } })`** serves scripts under **`/__webspresso/client-runtime/`** and exposes **`clientRuntime`** in Nunjucks; layouts can include **`views/partials/webspresso-client-runtime.njk`**. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`**. Demo: **`examples/alpine-swup-demo/`**. Details: **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
20
21
  - **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
21
22
 
22
23
  ## Installation
@@ -357,8 +358,9 @@ Creates and configures the Express app.
357
358
  - `false`: Disable Helmet
358
359
  - `Object`: Custom Helmet configuration (merged with defaults)
359
360
  - `middlewares` (optional): Named middleware registry for routes
361
+ - `clientRuntime` (optional): **`{ alpine?: boolean | object, swup?: boolean | object }`**. When either flag is enabled, mounts vendored Alpine 3 / swup 4 (+ Head + Scripts plugins) at **`/__webspresso/client-runtime/*`** and passes resolved **`{ alpine, swup }`** into SSR templates as **`clientRuntime`**. Override with env **`WEBSPRESSO_ALPINE`** / **`WEBSPRESSO_SWUP`** (`1` or `true`). Package exports **`resolveClientRuntime`** and **`CLIENT_RUNTIME_BASE`**. See **[Client runtime](#client-runtime)** below and **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
360
362
  - `auth` (optional): `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`require('webspresso/core/auth')`). Registers session + cookie parsing, attaches **`req.auth`** / **`req.user`**, and injects named route middleware **`auth`** and **`guest`** (do not reuse those names for custom handlers if you pass `auth`). See **[`doc/index.html#authentication`](doc/index.html#authentication)**.
361
- - `setupRoutes(app, ctx)` (optional): Register custom Express routes after file routes and plugin `onRoutesReady`, before the 404 handler — use for login/logout handlers when using the auth module; **`ctx.authMiddleware`** exposes `requireAuth`, `requireGuest`, etc.
363
+ - `setupRoutes(app, ctx)` (optional): Register custom Express routes after file routes and plugin `onRoutesReady`, before the 404 handler — use for login/logout handlers when using the auth module; **`ctx.authMiddleware`** exposes `requireAuth`, `requireGuest`, etc.; **`ctx.clientRuntime`** is **`{ alpine, swup }`**
362
364
 
363
365
  **Example with middlewares:**
364
366
 
@@ -423,6 +425,27 @@ middlewares: {
423
425
 
424
426
  Plain `(req, res, next) => …` handlers still work as today. Tuple form **requires** a factory for that name (you get a clear error if you mix a plain handler with `['name', opts]`).
425
427
 
428
+ ### Client runtime
429
+
430
+ Alpine.js + swup — opt-in progressive enhancement for SSR pages:
431
+
432
+ - **`clientRuntime: { alpine: true, swup: true }`** (each can be toggled independently). Default is off; no scripts are injected when both are disabled.
433
+ - Include **`{% include "partials/webspresso-client-runtime.njk" %}`** in your layout (copy from the npm package’s **`views/partials/`** or the framework repo). When **swup** is on, wrap the main content in **`<main id="swup">…</main>`** so transitions replace the correct region.
434
+ - **swup** uses Head + Scripts plugins; **Alpine** is re-bound after each visit via **`Alpine.initTree`** on the container. Use **`data-no-swup`** on links for a full page load. Paths **`/_admin`** and **`/_webspresso`** are ignored by the default bootstrap; the admin panel and dev dashboard stay separate Mithril apps.
435
+ - Dynamic UI can call **`pages/api/*`** from Alpine with **`fetch`** (see **`examples/alpine-swup-demo/`**).
436
+ - **Helmet / CSP**: production **`script-src 'self'`** works for **`/__webspresso/client-runtime/`**; some Alpine builds may need **`unsafe-eval`** — validate for your version or use a stricter build.
437
+
438
+ ```javascript
439
+ const { createApp } = require('webspresso');
440
+
441
+ const { app } = createApp({
442
+ pagesDir: './pages',
443
+ viewsDir: './views',
444
+ publicDir: './public',
445
+ clientRuntime: { alpine: true, swup: true },
446
+ });
447
+ ```
448
+
426
449
  ### App context (`req.db`, `getDb`, `attachDbMiddleware`)
427
450
 
428
451
  With **`createApp({ db })`**, file-based **API** routes (`pages/api/*.js`) get the same ORM instance on **`req.db`** before your **`middleware`** array and the handler run — no extra `require` in the handler:
package/index.d.ts CHANGED
@@ -45,6 +45,11 @@ export interface CreateAppOptions {
45
45
  timeout?: string | false;
46
46
  auth?: unknown;
47
47
  db?: DatabaseInstance | null;
48
+ /** Opt-in Alpine / swup assets under `/__webspresso/client-runtime/*`. Env: WEBSPRESSO_ALPINE, WEBSPRESSO_SWUP. */
49
+ clientRuntime?: {
50
+ alpine?: boolean | Record<string, unknown>;
51
+ swup?: boolean | Record<string, unknown>;
52
+ };
48
53
  setupRoutes?: (app: Application, ctx: SetupRoutesContext) => void;
49
54
  [key: string]: unknown;
50
55
  }
@@ -54,6 +59,7 @@ export interface SetupRoutesContext {
54
59
  authMiddleware?: RequestHandler;
55
60
  pluginManager: PluginManager;
56
61
  options: CreateAppOptions;
62
+ clientRuntime: { alpine: boolean; swup: boolean };
57
63
  }
58
64
 
59
65
  export interface CreateAppResult {
package/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  const { createApp } = require('./src/server');
6
+ const { resolveClientRuntime } = require('./src/client-runtime/resolve');
7
+ const { CLIENT_RUNTIME_BASE } = require('./src/client-runtime/mount');
6
8
  const {
7
9
  attachDbMiddleware,
8
10
  getAppContext,
@@ -43,6 +45,8 @@ const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlu
43
45
  module.exports = {
44
46
  // Main API
45
47
  createApp,
48
+ resolveClientRuntime,
49
+ CLIENT_RUNTIME_BASE,
46
50
 
47
51
  attachDbMiddleware,
48
52
  getAppContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -42,9 +42,13 @@
42
42
  "utils/",
43
43
  "core/",
44
44
  "plugins/",
45
- "templates/"
45
+ "templates/",
46
+ "views/partials/webspresso-client-runtime.njk"
46
47
  ],
47
48
  "dependencies": {
49
+ "@swup/head-plugin": "^2.3.1",
50
+ "@swup/scripts-plugin": "^2.1.0",
51
+ "alpinejs": "^3.15.11",
48
52
  "bcrypt": "^5.1.1",
49
53
  "commander": "^11.1.0",
50
54
  "connect-timeout": "^1.9.1",
@@ -57,6 +61,7 @@
57
61
  "knex": "^3.1.0",
58
62
  "nunjucks": "^3.2.4",
59
63
  "sharp": "^0.33.5",
64
+ "swup": "^4.8.3",
60
65
  "zod": "^3.23.0",
61
66
  "zod-to-json-schema": "^3.25.2"
62
67
  },
@@ -85,10 +90,10 @@
85
90
  }
86
91
  },
87
92
  "devDependencies": {
88
- "@types/express": "^4.17.21",
89
- "@types/node": "^20.14.0",
90
93
  "@faker-js/faker": "^9.9.0",
91
94
  "@playwright/test": "^1.48.0",
95
+ "@types/express": "^4.17.21",
96
+ "@types/node": "^20.14.0",
92
97
  "@vitest/coverage-v8": "^3.0.0",
93
98
  "better-sqlite3": "^11.10.0",
94
99
  "chokidar": "^3.5.3",
@@ -0,0 +1,34 @@
1
+ /* global Swup, SwupHeadPlugin, SwupScriptsPlugin, Alpine */
2
+ (function () {
3
+ if (typeof Swup === 'undefined' || typeof SwupHeadPlugin === 'undefined' || typeof SwupScriptsPlugin === 'undefined' || typeof Alpine === 'undefined') {
4
+ return;
5
+ }
6
+
7
+ function ignoreVisit(url, ctx) {
8
+ var el = ctx && ctx.el;
9
+ if (el && el.closest && el.closest('[data-no-swup]')) return true;
10
+ try {
11
+ var u = new URL(url, window.location.origin);
12
+ var p = u.pathname;
13
+ if (p.indexOf('/_admin') === 0 || p.indexOf('/_webspresso') === 0) return true;
14
+ } catch (e) {
15
+ /* ignore */
16
+ }
17
+ return false;
18
+ }
19
+
20
+ var swup = new Swup({
21
+ containers: ['#swup'],
22
+ plugins: [new SwupHeadPlugin(), new SwupScriptsPlugin()],
23
+ ignoreVisit: ignoreVisit,
24
+ });
25
+
26
+ swup.hooks.on('content:replace', function () {
27
+ var root = document.querySelector('#swup');
28
+ if (root && typeof Alpine.initTree === 'function') {
29
+ Alpine.initTree(root);
30
+ }
31
+ });
32
+
33
+ window.__webspressoSwup = swup;
34
+ })();
@@ -0,0 +1,26 @@
1
+ /* global Swup, SwupHeadPlugin, SwupScriptsPlugin */
2
+ (function () {
3
+ if (typeof Swup === 'undefined' || typeof SwupHeadPlugin === 'undefined' || typeof SwupScriptsPlugin === 'undefined') {
4
+ return;
5
+ }
6
+
7
+ function ignoreVisit(url, ctx) {
8
+ var el = ctx && ctx.el;
9
+ if (el && el.closest && el.closest('[data-no-swup]')) return true;
10
+ try {
11
+ var u = new URL(url, window.location.origin);
12
+ var p = u.pathname;
13
+ if (p.indexOf('/_admin') === 0 || p.indexOf('/_webspresso') === 0) return true;
14
+ } catch (e) {
15
+ /* ignore */
16
+ }
17
+ return false;
18
+ }
19
+
20
+ var swup = new Swup({
21
+ containers: ['#swup'],
22
+ plugins: [new SwupHeadPlugin(), new SwupScriptsPlugin()],
23
+ ignoreVisit: ignoreVisit,
24
+ });
25
+ window.__webspressoSwup = swup;
26
+ })();
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Mount Express routes that serve vendored Alpine / Swup UMD builds from node_modules
3
+ * and framework bootstrap scripts from src/client-runtime/.
4
+ */
5
+
6
+ const path = require('path');
7
+ const express = require('express');
8
+
9
+ const CLIENT_RUNTIME_BASE = '/__webspresso/client-runtime';
10
+
11
+ /** Resolve a file next to the package's resolved main entry (works with package "exports"). */
12
+ function pkgFileFromMain(pkg, ...segments) {
13
+ const entry = require.resolve(pkg);
14
+ return path.join(path.dirname(entry), ...segments);
15
+ }
16
+
17
+ /**
18
+ * @param {import('express').Express} app
19
+ * @param {{ alpine: boolean, swup: boolean }} flags
20
+ */
21
+ function mountClientRuntime(app, flags) {
22
+ if (!flags || (!flags.alpine && !flags.swup)) return;
23
+
24
+ const router = express.Router();
25
+
26
+ function send(res, filePath) {
27
+ res.type('application/javascript');
28
+ res.sendFile(filePath);
29
+ }
30
+
31
+ if (flags.alpine) {
32
+ router.get('/alpine.min.js', (req, res) => {
33
+ send(res, pkgFileFromMain('alpinejs', 'cdn.min.js'));
34
+ });
35
+ }
36
+
37
+ if (flags.swup) {
38
+ router.get('/swup.umd.js', (req, res) => {
39
+ send(res, pkgFileFromMain('swup', 'Swup.umd.js'));
40
+ });
41
+ router.get('/swup-head-plugin.umd.js', (req, res) => {
42
+ send(res, pkgFileFromMain('@swup/head-plugin', 'index.umd.js'));
43
+ });
44
+ router.get('/swup-scripts-plugin.umd.js', (req, res) => {
45
+ send(res, pkgFileFromMain('@swup/scripts-plugin', 'index.umd.js'));
46
+ });
47
+ const runtimeDir = __dirname;
48
+ if (flags.alpine) {
49
+ router.get('/bootstrap-alpine-swup.js', (req, res) => {
50
+ send(res, path.join(runtimeDir, 'bootstrap-alpine-swup.js'));
51
+ });
52
+ } else {
53
+ router.get('/bootstrap-swup.js', (req, res) => {
54
+ send(res, path.join(runtimeDir, 'bootstrap-swup.js'));
55
+ });
56
+ }
57
+ }
58
+
59
+ app.use(CLIENT_RUNTIME_BASE, router);
60
+ }
61
+
62
+ module.exports = {
63
+ mountClientRuntime,
64
+ CLIENT_RUNTIME_BASE,
65
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Resolve clientRuntime flags from createApp({ clientRuntime }) and optional env overrides.
3
+ * Env: WEBSPRESSO_ALPINE=1|true, WEBSPRESSO_SWUP=1|true (override explicit false from options when set).
4
+ */
5
+
6
+ function envTruthy(name) {
7
+ const v = process.env[name];
8
+ if (v == null || v === '') return undefined;
9
+ return v === '1' || /^true$/i.test(String(v));
10
+ }
11
+
12
+ function optionEnabled(v) {
13
+ if (v === true) return true;
14
+ if (v && typeof v === 'object') return true;
15
+ return false;
16
+ }
17
+
18
+ /**
19
+ * @param {object} [options]
20
+ * @param {object} [options.clientRuntime]
21
+ * @param {boolean|object} [options.clientRuntime.alpine]
22
+ * @param {boolean|object} [options.clientRuntime.swup]
23
+ * @returns {{ alpine: boolean, swup: boolean }}
24
+ */
25
+ function resolveClientRuntime(options = {}) {
26
+ const cr = options.clientRuntime;
27
+ let alpine = false;
28
+ let swup = false;
29
+ if (cr && typeof cr === 'object') {
30
+ alpine = optionEnabled(cr.alpine);
31
+ swup = optionEnabled(cr.swup);
32
+ }
33
+ const envA = envTruthy('WEBSPRESSO_ALPINE');
34
+ const envS = envTruthy('WEBSPRESSO_SWUP');
35
+ if (envA !== undefined) alpine = envA;
36
+ if (envS !== undefined) swup = envS;
37
+ return { alpine, swup };
38
+ }
39
+
40
+ module.exports = { resolveClientRuntime };
@@ -365,10 +365,22 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
365
365
  * @param {Object} options.pluginManager - Plugin manager instance
366
366
  * @param {boolean} options.silent - Suppress console output
367
367
  * @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
368
+ * @param {{ alpine?: boolean, swup?: boolean }} [options.clientRuntime] - Passed to Nunjucks as `clientRuntime` (default both false)
368
369
  * @returns {Array} Route metadata for plugins
369
370
  */
370
371
  function mountPages(app, options) {
371
- const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false, db = null } = options;
372
+ const {
373
+ pagesDir,
374
+ nunjucks,
375
+ middlewares = {},
376
+ pluginManager = null,
377
+ silent = false,
378
+ db = null,
379
+ clientRuntime: clientRuntimeOpt = null,
380
+ } = options;
381
+ const clientRuntime = clientRuntimeOpt && typeof clientRuntimeOpt === 'object'
382
+ ? { alpine: !!clientRuntimeOpt.alpine, swup: !!clientRuntimeOpt.swup }
383
+ : { alpine: false, swup: false };
372
384
  const isDev = process.env.NODE_ENV !== 'production';
373
385
  const log = silent ? () => {} : console.log.bind(console);
374
386
 
@@ -548,7 +560,8 @@ function mountPages(app, options) {
548
560
  indexable: true,
549
561
  canonical: null
550
562
  },
551
- fsy: { ...baseHelpers, ...pluginHelpers }
563
+ fsy: { ...baseHelpers, ...pluginHelpers },
564
+ clientRuntime,
552
565
  };
553
566
 
554
567
  // Execute hooks: onRequest
@@ -612,6 +625,7 @@ function mountPages(app, options) {
612
625
  locale: ctx.locale,
613
626
  t: ctx.t,
614
627
  fsy: ctx.fsy,
628
+ clientRuntime: ctx.clientRuntime,
615
629
  req: {
616
630
  path: req.path,
617
631
  query: req.query,
package/src/server.js CHANGED
@@ -9,6 +9,8 @@ const nunjucks = require('nunjucks');
9
9
  const timeout = require('connect-timeout');
10
10
 
11
11
  const { setAppContext } = require('./app-context');
12
+ const { mountClientRuntime } = require('./client-runtime/mount');
13
+ const { resolveClientRuntime } = require('./client-runtime/resolve');
12
14
  const { mountPages } = require('./file-router');
13
15
  const { configureAssets, createHelpers, getScriptInjector } = require('./helpers');
14
16
  const { createPluginManager } = require('./plugin-manager');
@@ -258,6 +260,7 @@ function haltOnTimedout(req, res, next) {
258
260
  * @param {string|boolean} options.timeout - Request timeout (default: '30s', false to disable)
259
261
  * @param {Object} options.auth - Authentication manager instance (from createAuth)
260
262
  * @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
263
+ * @param {Object} [options.clientRuntime] - Optional client assets: `{ alpine?: boolean|object, swup?: boolean|object }`. Overridable by env `WEBSPRESSO_ALPINE` / `WEBSPRESSO_SWUP` (=1 or true). Serves `/__webspresso/client-runtime/*` when either flag is on.
261
264
  * @param {function(import('express').Express, Object): void} [options.setupRoutes] - Called after file routes and plugins, before 404 handler
262
265
  * @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
263
266
  */
@@ -294,6 +297,8 @@ function createApp(options = {}) {
294
297
  throw new Error('pagesDir is required');
295
298
  }
296
299
 
300
+ const clientRuntime = resolveClientRuntime(options);
301
+
297
302
  setAppContext({ db: options.db ?? null });
298
303
 
299
304
  const app = express();
@@ -378,7 +383,9 @@ function createApp(options = {}) {
378
383
  etag: true
379
384
  }));
380
385
  }
381
-
386
+
387
+ mountClientRuntime(app, clientRuntime);
388
+
382
389
  // Configure Nunjucks
383
390
  const templateDirs = viewsDir ? [pagesDir, viewsDir] : [pagesDir];
384
391
 
@@ -439,7 +446,8 @@ function createApp(options = {}) {
439
446
  middlewares,
440
447
  pluginManager,
441
448
  silent: isTest,
442
- db: options.db ?? null
449
+ db: options.db ?? null,
450
+ clientRuntime,
443
451
  });
444
452
 
445
453
  // Set route metadata in plugin manager
@@ -480,6 +488,7 @@ function createApp(options = {}) {
480
488
  authMiddleware,
481
489
  pluginManager,
482
490
  options,
491
+ clientRuntime,
483
492
  });
484
493
  }
485
494
 
@@ -66,8 +66,9 @@ project/
66
66
  | `timeout` | e.g. `'30s'` or `false` |
67
67
  | `helmet` | `true` / `false` / object |
68
68
  | `assets` | `{ version, manifestPath, prefix }` for `fsy.asset` / `fsy.css` / `fsy.js` |
69
+ | `clientRuntime` | Opt-in `{ alpine?, swup? }` — serves `/__webspresso/client-runtime/*`, template `clientRuntime`, partial `views/partials/webspresso-client-runtime.njk`, `<main id="swup">` when swup on. Env: `WEBSPRESSO_ALPINE`, `WEBSPRESSO_SWUP`. |
69
70
  | `auth` | Auth manager from `createAuth` (session routes) |
70
- | `setupRoutes(app, ctx)` | **Register custom Express routes here** — runs **after** file routes and plugins’ `onRoutesReady`, **before** 404. Do not rely on `app.get` *after* `createApp` returns unless routes are appended before the 404 middleware (see [`src/server.js`](../../../src/server.js)). |
71
+ | `setupRoutes(app, ctx)` | **Register custom Express routes here** — runs **after** file routes and plugins’ `onRoutesReady`, **before** 404. `ctx.clientRuntime` included. Do not rely on `app.get` *after* `createApp` returns unless routes are appended before the 404 middleware (see [`src/server.js`](../../../src/server.js)). |
71
72
 
72
73
  **Returns:** `{ app, nunjucksEnv, pluginManager, authMiddleware? }` (and related).
73
74
 
@@ -0,0 +1,15 @@
1
+ {# Served by createApp when clientRuntime.alpine / .swup are enabled. See src/client-runtime/. #}
2
+ {% if clientRuntime and clientRuntime.alpine %}
3
+ <style>[x-cloak] { display: none !important; }</style>
4
+ <script defer src="/__webspresso/client-runtime/alpine.min.js"></script>
5
+ {% endif %}
6
+ {% if clientRuntime and clientRuntime.swup %}
7
+ <script defer src="/__webspresso/client-runtime/swup.umd.js"></script>
8
+ <script defer src="/__webspresso/client-runtime/swup-head-plugin.umd.js"></script>
9
+ <script defer src="/__webspresso/client-runtime/swup-scripts-plugin.umd.js"></script>
10
+ {% if clientRuntime.alpine %}
11
+ <script defer src="/__webspresso/client-runtime/bootstrap-alpine-swup.js"></script>
12
+ {% else %}
13
+ <script defer src="/__webspresso/client-runtime/bootstrap-swup.js"></script>
14
+ {% endif %}
15
+ {% endif %}