webspresso 0.0.72 → 0.0.74

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.
@@ -140,6 +140,69 @@ function extractMethodFromFilename(filename) {
140
140
  return result;
141
141
  }
142
142
 
143
+ /**
144
+ * Whether `load()` return values for `stylesheets` and `scripts` are promoted to
145
+ * `pageHead` in Nunjucks (see `createApp({ pageAssets })`).
146
+ * @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}|null|undefined} raw
147
+ * @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean }}
148
+ */
149
+ function resolvePageAssets(raw) {
150
+ if (raw === true) {
151
+ return { enabled: true, stylesheets: true, scripts: true };
152
+ }
153
+ if (raw == null || raw === false) {
154
+ return { enabled: false, stylesheets: false, scripts: false };
155
+ }
156
+ if (typeof raw === 'object') {
157
+ const on = raw.enabled !== false;
158
+ if (!on) {
159
+ return { enabled: false, stylesheets: false, scripts: false };
160
+ }
161
+ return {
162
+ enabled: true,
163
+ stylesheets: raw.stylesheets !== false,
164
+ scripts: raw.scripts !== false,
165
+ };
166
+ }
167
+ return { enabled: false, stylesheets: false, scripts: false };
168
+ }
169
+
170
+ /**
171
+ * @param {unknown} v
172
+ * @returns {unknown[]}
173
+ */
174
+ function toList(v) {
175
+ if (v == null) return [];
176
+ return Array.isArray(v) ? v : [v];
177
+ }
178
+
179
+ /**
180
+ * @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean }} cfg
181
+ * @param {Object} data
182
+ * @returns {{ data: Object, pageHead: { stylesheets: unknown[], scripts: unknown[] }|null, pageAssets: boolean }}
183
+ */
184
+ function applyPageAssetsToTemplateData(cfg, data) {
185
+ if (!cfg || !cfg.enabled) {
186
+ return { data, pageHead: null, pageAssets: false };
187
+ }
188
+ const out = { ...data };
189
+ let styles = [];
190
+ let scriptItems = [];
191
+ if (cfg.stylesheets && Object.prototype.hasOwnProperty.call(out, 'stylesheets')) {
192
+ styles = toList(out.stylesheets);
193
+ delete out.stylesheets;
194
+ }
195
+ if (cfg.scripts && Object.prototype.hasOwnProperty.call(out, 'scripts')) {
196
+ scriptItems = toList(out.scripts);
197
+ delete out.scripts;
198
+ }
199
+ return {
200
+ data: out,
201
+ pageHead: { stylesheets: styles, scripts: scriptItems },
202
+ pageAssets: true,
203
+ };
204
+ }
205
+
143
206
  /**
144
207
  * Recursively scan a directory for files
145
208
  * @param {string} dir - Directory to scan
@@ -439,6 +502,7 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
439
502
  * @param {boolean} options.silent - Suppress console output
440
503
  * @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
441
504
  * @param {{ alpine?: boolean, swup?: boolean }} [options.clientRuntime] - Passed to Nunjucks as `clientRuntime` (default both false)
505
+ * @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If set, `load()` may return `stylesheets` / `scripts` promoted to `pageHead` in templates
442
506
  * @returns {Array} Route metadata for plugins
443
507
  */
444
508
  function mountPages(app, options) {
@@ -450,7 +514,9 @@ function mountPages(app, options) {
450
514
  silent = false,
451
515
  db = null,
452
516
  clientRuntime: clientRuntimeOpt = null,
517
+ pageAssets: pageAssetsOpt = null,
453
518
  } = options;
519
+ const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
454
520
  const clientRuntime = clientRuntimeOpt && typeof clientRuntimeOpt === 'object'
455
521
  ? { alpine: !!clientRuntimeOpt.alpine, swup: !!clientRuntimeOpt.swup }
456
522
  : { alpine: false, swup: false };
@@ -686,9 +752,9 @@ function mountPages(app, options) {
686
752
  await executeHook(globalHooks, 'beforeRender', ctx);
687
753
  await executeHook(routeHooks, 'beforeRender', ctx);
688
754
 
689
- // Render the template
690
- const templatePath = route.file.split(path.sep).join('/');
691
- const html = nunjucks.render(templatePath, {
755
+ const pageAssetBundle = applyPageAssetsToTemplateData(pageAssetsResolved, ctx.data);
756
+ ctx.data = pageAssetBundle.data;
757
+ const renderContext = {
692
758
  ...ctx.data,
693
759
  meta: ctx.meta,
694
760
  locale: ctx.locale,
@@ -700,7 +766,15 @@ function mountPages(app, options) {
700
766
  query: req.query,
701
767
  params: req.params
702
768
  }
703
- });
769
+ };
770
+ if (pageAssetBundle.pageAssets) {
771
+ renderContext.pageAssets = true;
772
+ renderContext.pageHead = pageAssetBundle.pageHead;
773
+ }
774
+
775
+ // Render the template
776
+ const templatePath = route.file.split(path.sep).join('/');
777
+ const html = nunjucks.render(templatePath, renderContext);
704
778
 
705
779
  // Execute hooks: afterRender
706
780
  ctx.html = html;
@@ -769,5 +843,7 @@ module.exports = {
769
843
  resolveMiddlewares,
770
844
  routeRegistrationMeta,
771
845
  compareRouteRegistrationOrder,
846
+ resolvePageAssets,
847
+ applyPageAssetsToTemplateData,
772
848
  };
773
849
 
package/src/server.js CHANGED
@@ -261,6 +261,7 @@ function haltOnTimedout(req, res, next) {
261
261
  * @param {Object} options.auth - Authentication manager instance (from createAuth)
262
262
  * @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
263
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.
264
+ * @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If truthy, route `load()` return values for `stylesheets` and `scripts` are reserved: removed from the root template context and passed as `pageHead` to Nunjucks, with `pageAssets: true` (for layout to emit `<link>` / `<script>`). Default off.
264
265
  * @param {function(import('express').Express, Object): void} [options.setupRoutes] - Called after file routes and plugins, before 404 handler
265
266
  * @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
266
267
  */
@@ -448,6 +449,7 @@ function createApp(options = {}) {
448
449
  silent: isTest,
449
450
  db: options.db ?? null,
450
451
  clientRuntime,
452
+ pageAssets: options.pageAssets,
451
453
  });
452
454
 
453
455
  // Set route metadata in plugin manager
@@ -68,6 +68,7 @@ project/
68
68
  | `timeout` | e.g. `'30s'` or `false` |
69
69
  | `helmet` | `true` / `false` / object |
70
70
  | `assets` | `{ version, manifestPath, prefix }` for `fsy.asset` / `fsy.css` / `fsy.js` |
71
+ | `pageAssets` | Opt-in **`true`** or **`{ enabled?, stylesheets?, scripts? }`**. When on, route **`load()`** may return reserved keys **`stylesheets`** (string or list; also `{ href, media? }` objects) and **`scripts`** (string, `{ src, defer?, async?, type? }`, or list). They are removed from the root Nunjucks context and passed as **`pageHead`** with **`pageAssets: true`**. The app layout must print them (see [`views/layout.njk`](../../../views/layout.njk) in the package). Default **off** — `stylesheets` / `scripts` in **`load()`** behave as normal data keys. |
71
72
  | `clientRuntime` | Opt-in **`{ alpine?: boolean \| object, swup?: boolean \| object }`**. Serves **`/__webspresso/client-runtime/*`** (Alpine 3, swup 4 + Head + Scripts plugins + bootstrap). Template context **`clientRuntime`**; include [`views/partials/webspresso-client-runtime.njk`](../../../views/partials/webspresso-client-runtime.njk) and set **`<main id="swup">`** when swup is on. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`** (`1` or `true`). Admin / dev dashboard HTML is unchanged (Mithril). Use **`data-no-swup`** on links for full page loads. HTMX is not used. |
72
73
  | `auth` | `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`webspresso/core/auth`). Mounts cookie parser + **`express-session`** + per-request **`authenticate`**; sets **`req.user`**, **`req.auth`**. Injects named route middleware **`auth`** and **`guest`** (overwrites same keys in `middlewares` if you passed both — avoid reusing those names for custom handlers). |
73
74
  | `setupRoutes(app, ctx)` | **Register custom Express routes here** — runs **after** file routes and plugins’ `onRoutesReady`, **before** 404. **`ctx.clientRuntime`** is the resolved flags. **`ctx.authMiddleware`** is set when `auth` was passed (guards: `requireAuth`, `requireGuest`, `requireCan`, `requireVerified`, …). Do not rely on `app.get` *after* `createApp` returns unless routes are appended before the 404 middleware (see [`src/server.js`](../../../src/server.js)). |
@@ -91,8 +92,10 @@ project/
91
92
  - **Route config:** `middleware: ['auth']` (must be logged in) or `['guest']` (logged-out only). For JSON APIs mounted in **`setupRoutes`**, use **`ctx.authMiddleware.requireAuth({ api: true })`** for 401 JSON instead of redirect.
92
93
  - **Login page pitfall:** a **`pages/login.njk`** can register **before** `setupRoutes` and bypass **`requireGuest`**. Prefer login GET/POST in **`setupRoutes`** with templates under **`views/`** only, or omit **`pages/login.njk`** — see [`tests/e2e/auth.spec.js`](../../../tests/e2e/auth.spec.js).
93
94
  - **Admin panel** uses a **separate** session (`req.session.adminUser`, `/_admin/api/auth/*`); it does **not** replace **`createApp({ auth })`** for site users.
95
+ - **Site users in the admin UI (`userManagement`):** Opt-in on **`adminPanelPlugin`**. Set **`userManagement: { enabled: true, model: 'User', fields?: { ... } }`** so the SPA shows **Users** (routes like **`/_admin/users`**, **`/_admin/users/new`**, **`/_admin/users/:id/edit`** — same Mithril shell as the rest of the panel). The **`model`** must be the ORM model your site auth uses (e.g. **`quickAuth({ userModel: 'User', ... })`** / **`createAuth`** adapters reading the same table). Pass **`auth: authManager`** with the **same** **`AuthManager`** instance as **`createApp({ auth: authManager })`** when you want **Active Sessions** / revoke APIs (**`rememberTokens`** / **`remember_me`**); without **`auth`**, list/create/update/delete users still work via **`db.getRepository(model)`**, but session endpoints return empty or “not enabled”.
96
+ - **Wiring:** `plugins: [ adminPanelPlugin({ db, auth: authManager, userManagement: { enabled: true, model: 'User' } }) ]` alongside `createApp({ ..., auth: authManager })`. Admin staff log in at **`/_admin`**; end users use your normal site login — two different cookies/sessions.
94
97
 
95
- Longer narrative: **[`doc/index.html#authentication`](../../../doc/index.html#authentication)** · README **Authentication (session)**.
98
+ Longer narrative: **[`doc/index.html#authentication`](../../../doc/index.html#authentication)** · **[`#admin-user-management`](../../../doc/index.html#admin-user-management)** · README **Authentication (session)** and **Admin Panel Plugin**.
96
99
 
97
100
  ---
98
101
 
@@ -200,7 +203,7 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
200
203
  | `dashboardPlugin` | Dev route `/_webspresso` — route list |
201
204
  | `sitemapPlugin` | `/sitemap.xml`, robots; optional DB-driven URLs |
202
205
  | `analyticsPlugin` | GA / GTM / Yandex / Bing / Facebook — `fsy` helpers |
203
- | `adminPanelPlugin` | SPA admin CRUD — needs `db`; optional `uploadUrl` or infer from `uploadPlugin` order |
206
+ | `adminPanelPlugin` | SPA admin CRUD — needs **`db`**; optional **`uploadUrl`** (or infer from **`uploadPlugin`**); optional **`userManagement: { enabled, model, fields }`** + **`auth`** (same **`AuthManager`** as **`createApp({ auth })`**) for site-user CRUD + remember-me session UI — see **Session authentication** above |
204
207
  | `uploadPlugin` | `POST` multipart (`multer`), `createLocalFileProvider` or custom `provider`; set **`mimeAllowlist`** / **`maxBytes`** in production |
205
208
  | `siteAnalyticsPlugin` | Self-hosted page views + admin charts |
206
209
  | `auditLogPlugin` | Admin mutation audit trail |