webspresso 0.0.66 → 0.0.68

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
@@ -128,6 +129,17 @@ If you select a database:
128
129
  - `models/` directory is created
129
130
  - `DATABASE_URL` is added to `.env.example` with a template
130
131
 
132
+ **Scaffold: `config/` and environment files**
133
+
134
+ New projects include:
135
+
136
+ - **`config/load-env.js`** — loads, in order, `.env`, `.env.local`, `.env.${NODE_ENV}`, and `.env.${NODE_ENV}.local` (each file overrides keys from earlier ones).
137
+ - **`config/env.schema.js`** — validates `process.env` with **Zod** before the app starts (`PORT`, `NODE_ENV`, i18n vars, `BASE_URL`, optional `DATABASE_URL`).
138
+ - **`config/app.js`** — returns `createApp()` options (paths; if `webspresso.db.js` exists, also **`createDatabase`** as `db`).
139
+ - **`server.js`** — calls `loadEnv()`, then `createApp(getCreateAppOptions())`, then `listen` using the parsed `PORT`.
140
+
141
+ Copy **`.env.example`** to **`.env`** (and use `.env.local` for machine-specific overrides). Patterns such as `.env.development.local` are gitignored via `.env.*.local`.
142
+
131
143
  **Seed Data Generation:**
132
144
  After selecting a database, you'll be asked if you want to generate seed data:
133
145
  - If yes, `@faker-js/faker` is added to dependencies
@@ -357,8 +369,9 @@ Creates and configures the Express app.
357
369
  - `false`: Disable Helmet
358
370
  - `Object`: Custom Helmet configuration (merged with defaults)
359
371
  - `middlewares` (optional): Named middleware registry for routes
372
+ - `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
373
  - `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.
374
+ - `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
375
 
363
376
  **Example with middlewares:**
364
377
 
@@ -423,6 +436,27 @@ middlewares: {
423
436
 
424
437
  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
438
 
439
+ ### Client runtime
440
+
441
+ Alpine.js + swup — opt-in progressive enhancement for SSR pages:
442
+
443
+ - **`clientRuntime: { alpine: true, swup: true }`** (each can be toggled independently). Default is off; no scripts are injected when both are disabled.
444
+ - 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.
445
+ - **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.
446
+ - Dynamic UI can call **`pages/api/*`** from Alpine with **`fetch`** (see **`examples/alpine-swup-demo/`**).
447
+ - **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.
448
+
449
+ ```javascript
450
+ const { createApp } = require('webspresso');
451
+
452
+ const { app } = createApp({
453
+ pagesDir: './pages',
454
+ viewsDir: './views',
455
+ publicDir: './public',
456
+ clientRuntime: { alpine: true, swup: true },
457
+ });
458
+ ```
459
+
426
460
  ### App context (`req.db`, `getDb`, `attachDbMiddleware`)
427
461
 
428
462
  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:
@@ -2018,6 +2052,45 @@ const { app } = createApp({
2018
2052
 
2019
2053
  List query parameters: `page`, `perPage`, `sort`, `order`, `include`, `trashed` (`only` / `include` when the model uses soft delete), plus **equality filters** on real columns (unknown keys are ignored).
2020
2054
 
2055
+ ### File upload plugin
2056
+
2057
+ Registers **POST** `multipart/form-data` (field name **`file`** by default) and stores the file via a pluggable provider. The framework ships with **`createLocalFileProvider({ destDir, publicBasePath })`** (writes to disk and returns a public URL path). Use **`mimeAllowlist`** / **`extensionAllowlist`** and **`maxBytes`** (default **10 MiB**) on the server; production apps should prefer an explicit MIME allowlist.
2058
+
2059
+ **Setup:**
2060
+
2061
+ ```javascript
2062
+ const { createApp, uploadPlugin, adminPanelPlugin } = require('webspresso');
2063
+
2064
+ const { app } = createApp({
2065
+ pagesDir: './pages',
2066
+ publicDir: './public',
2067
+ db,
2068
+ plugins: [
2069
+ uploadPlugin({
2070
+ path: '/api/upload',
2071
+ local: {
2072
+ destDir: './public/uploads',
2073
+ publicBasePath: '/uploads',
2074
+ },
2075
+ maxBytes: 10 * 1024 * 1024,
2076
+ mimeAllowlist: ['image/jpeg', 'image/png', 'application/pdf'],
2077
+ extensionAllowlist: ['jpg', 'jpeg', 'png', 'pdf'],
2078
+ middleware: [], // optional Express handlers (e.g. session auth)
2079
+ fieldName: 'file',
2080
+ }),
2081
+ adminPanelPlugin({
2082
+ db,
2083
+ // uploadUrl optional: defaults to app.get('webspresso.uploadPath') set by uploadPlugin
2084
+ }),
2085
+ ],
2086
+ });
2087
+ ```
2088
+
2089
+ - **ORM:** `zdb.file({ maxLength: 2048, nullable: true })` — string column for the stored public URL or path; migrations use `table.string(..., maxLength)`.
2090
+ - **Admin:** the panel reads **`settings.uploadUrl`** from the registry (set automatically when `uploadPlugin` is registered **before** `adminPanelPlugin`, or pass **`adminPanelPlugin({ uploadUrl: '/api/upload' })`**). File fields (`type: 'file'` or `customFields` type `file-upload`) POST to that URL with credentials.
2091
+ - **Response:** `{ url, publicUrl, key? }` — clients typically persist **`url`** / **`publicUrl`** in the model.
2092
+ - **Custom storage:** `uploadPlugin({ provider: { async put({ buffer, originalName, mimeType, size, req }) { return { publicUrl: '...' }; } } })`.
2093
+
2021
2094
  ### Health check plugin
2022
2095
 
2023
2096
  Exposes a lightweight **GET** endpoint for load balancers and orchestrators (Kubernetes, Docker healthcheck, etc.). **Enabled by default** in all environments; set `enabled: false` to turn it off.
@@ -107,6 +107,29 @@ function registerCommand(program) {
107
107
  warnings += 1;
108
108
  }
109
109
 
110
+ console.log('\nEnvironment files');
111
+ console.log('-----------------');
112
+
113
+ const envExamplePath = path.join(cwd, '.env.example');
114
+ const envPath = path.join(cwd, '.env');
115
+ if (fs.existsSync(envExamplePath)) {
116
+ line('✓', '.env.example present');
117
+ } else {
118
+ line('○', 'No .env.example (optional; new scaffolds include one)');
119
+ }
120
+ if (fs.existsSync(envPath)) {
121
+ line('✓', '.env present');
122
+ } else if (fs.existsSync(envExamplePath)) {
123
+ line('⚠', 'No .env — copy .env.example to .env if the app expects env vars');
124
+ warnings += 1;
125
+ } else {
126
+ line('○', 'No .env (fine if you do not use dotenv in this project)');
127
+ }
128
+ const loadEnvPath = path.join(cwd, 'config', 'load-env.js');
129
+ if (fs.existsSync(loadEnvPath)) {
130
+ line('✓', 'config/load-env.js present (dotenv chain scaffold)');
131
+ }
132
+
110
133
  console.log('\nDatabase config');
111
134
  console.log('---------------');
112
135
 
@@ -159,6 +159,7 @@ function registerCommand(program) {
159
159
  fs.mkdirSync(path.join(projectPath, 'pages', 'locales'), { recursive: true });
160
160
  fs.mkdirSync(path.join(projectPath, 'views'), { recursive: true });
161
161
  fs.mkdirSync(path.join(projectPath, 'public'), { recursive: true });
162
+ fs.mkdirSync(path.join(projectPath, 'config'), { recursive: true });
162
163
 
163
164
  // Create models directory if database is selected
164
165
  if (useDatabase && databaseType) {
@@ -177,7 +178,8 @@ function registerCommand(program) {
177
178
  },
178
179
  dependencies: {
179
180
  webspresso: '*',
180
- dotenv: '^16.3.1'
181
+ dotenv: '^16.3.1',
182
+ zod: '^3.23.0'
181
183
  }
182
184
  };
183
185
 
@@ -203,28 +205,108 @@ function registerCommand(program) {
203
205
  JSON.stringify(packageJson, null, 2) + '\n'
204
206
  );
205
207
 
206
- // Create server.js
207
- const serverJs = `require('dotenv').config();
208
- const { createApp } = require('webspresso');
208
+ // config/load-env.js — dotenv chain (last file wins for each key)
209
+ const loadEnvJs = `const fs = require('fs');
209
210
  const path = require('path');
211
+ const dotenv = require('dotenv');
212
+
213
+ /**
214
+ * Load env files in order: .env, .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local
215
+ * @param {string} [rootDir] Project root (default: parent of config/)
216
+ */
217
+ function loadEnv(rootDir) {
218
+ const root = rootDir || path.resolve(__dirname, '..');
219
+ const loadFile = (name) => {
220
+ const full = path.join(root, name);
221
+ if (fs.existsSync(full)) {
222
+ dotenv.config({ path: full, override: true });
223
+ }
224
+ };
225
+ loadFile('.env');
226
+ loadFile('.env.local');
227
+ const mode = process.env.NODE_ENV || 'development';
228
+ loadFile(\`.env.\${mode}\`);
229
+ loadFile(\`.env.\${mode}.local\`);
230
+ }
231
+
232
+ module.exports = { loadEnv };
233
+ `;
210
234
 
211
- const { app } = createApp({
212
- pagesDir: path.join(__dirname, 'pages'),
213
- viewsDir: path.join(__dirname, 'views'),
214
- publicDir: path.join(__dirname, 'public')
235
+ const envSchemaJs = `const { z } = require('zod');
236
+
237
+ const envSchema = z.object({
238
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
239
+ PORT: z.coerce.number().int().positive().default(3000),
240
+ DEFAULT_LOCALE: z.string().min(1).default('en'),
241
+ SUPPORTED_LOCALES: z.string().min(1).default('en,de'),
242
+ BASE_URL: z.string().url().default('http://localhost:3000'),
243
+ DATABASE_URL: z.string().optional(),
215
244
  });
216
245
 
217
- const PORT = process.env.PORT || 3000;
246
+ let _parsed = null;
247
+
248
+ function parseEnv() {
249
+ if (_parsed) return _parsed;
250
+ const result = envSchema.safeParse(process.env);
251
+ if (!result.success) {
252
+ console.error('Invalid environment variables:');
253
+ console.error(result.error.format());
254
+ process.exit(1);
255
+ }
256
+ _parsed = result.data;
257
+ return _parsed;
258
+ }
259
+
260
+ module.exports = { envSchema, parseEnv };
261
+ `;
262
+
263
+ const appConfigJs = `const path = require('path');
264
+ const fs = require('fs');
265
+ const { parseEnv } = require('./env.schema');
266
+
267
+ function getCreateAppOptions() {
268
+ parseEnv();
269
+ const rootDir = path.resolve(__dirname, '..');
270
+ const options = {
271
+ pagesDir: path.join(rootDir, 'pages'),
272
+ viewsDir: path.join(rootDir, 'views'),
273
+ publicDir: path.join(rootDir, 'public'),
274
+ };
275
+ const dbFile = path.join(rootDir, 'webspresso.db.js');
276
+ if (fs.existsSync(dbFile)) {
277
+ const { createDatabase } = require('webspresso');
278
+ const knexConfig = require(dbFile);
279
+ options.db = createDatabase(knexConfig);
280
+ }
281
+ return options;
282
+ }
283
+
284
+ module.exports = getCreateAppOptions;
285
+ `;
286
+
287
+ const serverJs = `const { loadEnv } = require('./config/load-env');
288
+ loadEnv();
218
289
 
219
- app.listen(PORT, () => {
220
- console.log(\`🚀 Server running at http://localhost:\${PORT}\`);
290
+ const { createApp } = require('webspresso');
291
+ const getCreateAppOptions = require('./config/app');
292
+ const { parseEnv } = require('./config/env.schema');
293
+
294
+ const env = parseEnv();
295
+ const { app } = createApp(getCreateAppOptions());
296
+
297
+ app.listen(env.PORT, () => {
298
+ console.log(\`🚀 Server running at http://localhost:\${env.PORT}\`);
221
299
  });
222
300
  `;
223
-
301
+
302
+ fs.writeFileSync(path.join(projectPath, 'config', 'load-env.js'), loadEnvJs);
303
+ fs.writeFileSync(path.join(projectPath, 'config', 'env.schema.js'), envSchemaJs);
304
+ fs.writeFileSync(path.join(projectPath, 'config', 'app.js'), appConfigJs);
224
305
  fs.writeFileSync(path.join(projectPath, 'server.js'), serverJs);
225
306
 
226
- // Create .env.example
227
- let envExample = `PORT=3000
307
+ // Create .env.example (see config/load-env.js for merge order)
308
+ let envExample = `# Copy to .env and adjust. Optional overrides: .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local
309
+ PORT=3000
228
310
  NODE_ENV=development
229
311
  DEFAULT_LOCALE=en
230
312
  SUPPORTED_LOCALES=en,de
@@ -308,10 +390,11 @@ BASE_URL=http://localhost:3000
308
390
  fs.writeFileSync(path.join(projectPath, 'seeds', 'index.js'), seedIndex);
309
391
  }
310
392
 
311
- // Create .gitignore
393
+ // Create .gitignore (.env optional: teams often commit a non-secret .env for local defaults)
312
394
  const gitignore = `node_modules/
313
395
  .env
314
396
  .env.local
397
+ .env.*.local
315
398
  .DS_Store
316
399
  coverage/
317
400
  *.log
@@ -482,10 +565,18 @@ Webspresso project
482
565
 
483
566
  \`\`\`bash
484
567
  npm install
568
+ cp .env.example .env
485
569
  npm run dev
486
570
  \`\`\`
487
571
 
488
572
  Visit http://localhost:3000
573
+
574
+ ## Configuration
575
+
576
+ - **\`config/load-env.js\`** — loads \`.env\`, \`.env.local\`, then \`.env.$NODE_ENV\` and \`.env.$NODE_ENV.local\` (later files override keys).
577
+ - **\`config/env.schema.js\`** — [Zod](https://zod.dev) schema for \`process.env\`; fails fast on invalid values.
578
+ - **\`config/app.js\`** — builds options passed to \`createApp()\` (paths; adds \`db\` when \`webspresso.db.js\` exists).
579
+ - **\`server.js\`** — calls \`loadEnv()\`, then \`createApp(getCreateAppOptions())\`.
489
580
  `;
490
581
 
491
582
  fs.writeFileSync(path.join(projectPath, 'README.md'), readme);
@@ -112,6 +112,12 @@ function generateColumnLine(columnName, meta) {
112
112
  parts.push(`table.text('${columnName}')`);
113
113
  break;
114
114
 
115
+ case 'file': {
116
+ const fileLen = meta.maxLength || 2048;
117
+ parts.push(`table.string('${columnName}', ${fileLen})`);
118
+ break;
119
+ }
120
+
115
121
  case 'float':
116
122
  parts.push(`table.float('${columnName}')`);
117
123
  break;
@@ -420,6 +420,25 @@ function createSchemaHelpers(z) {
420
420
  }, z);
421
421
  },
422
422
 
423
+ /**
424
+ * File / URL column (stored as varchar; value is public URL or path from upload)
425
+ * @param {Partial<import('./types').ColumnMeta>} [options={}]
426
+ * @returns {SchemaBuilder}
427
+ */
428
+ file(options = {}) {
429
+ const { maxLength = 2048, nullable = false, ...rest } = options;
430
+ let schema = z.string().max(maxLength);
431
+ if (nullable) {
432
+ schema = schema.nullable().optional();
433
+ }
434
+ return createSchemaBuilder(schema, {
435
+ type: 'file',
436
+ maxLength,
437
+ nullable,
438
+ ...rest,
439
+ }, z);
440
+ },
441
+
423
442
  /**
424
443
  * Integer column
425
444
  * @param {Partial<import('./types').ColumnMeta>} [options={}]
package/core/orm/types.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // ============================================================================
10
10
 
11
11
  /**
12
- * @typedef {'id'|'string'|'text'|'integer'|'bigint'|'float'|'decimal'|'boolean'|'date'|'datetime'|'timestamp'|'json'|'array'|'enum'|'uuid'|'nanoid'} ColumnType
12
+ * @typedef {'id'|'string'|'text'|'file'|'integer'|'bigint'|'float'|'decimal'|'boolean'|'date'|'datetime'|'timestamp'|'json'|'array'|'enum'|'uuid'|'nanoid'} ColumnType
13
13
  */
14
14
 
15
15
  /**
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 {
@@ -525,3 +531,33 @@ export interface RestResourcePluginOptions {
525
531
  export function restResourcePlugin(options?: RestResourcePluginOptions): WebspressoPlugin;
526
532
 
527
533
  export function ormCacheAdminPlugin(options: { db: DatabaseInstance }): WebspressoPlugin;
534
+
535
+ /** Multipart upload storage (e.g. local disk or S3). */
536
+ export interface UploadStorageProvider {
537
+ put(args: {
538
+ buffer?: Buffer;
539
+ stream?: NodeJS.ReadableStream;
540
+ originalName: string;
541
+ mimeType: string;
542
+ size: number;
543
+ req: Request;
544
+ }): Promise<{ publicUrl: string; key?: string }>;
545
+ }
546
+
547
+ export interface UploadPluginOptions {
548
+ path?: string;
549
+ provider?: UploadStorageProvider;
550
+ local?: { destDir?: string; publicBasePath?: string };
551
+ maxBytes?: number;
552
+ mimeAllowlist?: string[] | null;
553
+ extensionAllowlist?: string[] | null;
554
+ middleware?: RequestHandler | RequestHandler[];
555
+ fieldName?: string;
556
+ }
557
+
558
+ export function uploadPlugin(options?: UploadPluginOptions): WebspressoPlugin;
559
+
560
+ export function createLocalFileProvider(options?: {
561
+ destDir?: string;
562
+ publicBasePath?: string;
563
+ }): UploadStorageProvider;
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,
@@ -38,11 +40,25 @@ const {
38
40
  const orm = require('./core/orm');
39
41
 
40
42
  // Built-in plugins
41
- const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin, swaggerPlugin, healthCheckPlugin, restResourcePlugin, ormCacheAdminPlugin } = require('./plugins');
43
+ const {
44
+ schemaExplorerPlugin,
45
+ adminPanelPlugin,
46
+ siteAnalyticsPlugin,
47
+ auditLogPlugin,
48
+ recaptchaPlugin,
49
+ swaggerPlugin,
50
+ healthCheckPlugin,
51
+ restResourcePlugin,
52
+ ormCacheAdminPlugin,
53
+ uploadPlugin,
54
+ createLocalFileProvider,
55
+ } = require('./plugins');
42
56
 
43
57
  module.exports = {
44
58
  // Main API
45
59
  createApp,
60
+ resolveClientRuntime,
61
+ CLIENT_RUNTIME_BASE,
46
62
 
47
63
  attachDbMiddleware,
48
64
  getAppContext,
@@ -91,4 +107,6 @@ module.exports = {
91
107
  healthCheckPlugin,
92
108
  restResourcePlugin,
93
109
  ormCacheAdminPlugin,
110
+ uploadPlugin,
111
+ createLocalFileProvider,
94
112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
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",
@@ -55,8 +59,10 @@
55
59
  "helmet": "^7.2.0",
56
60
  "inquirer": "^8.2.6",
57
61
  "knex": "^3.1.0",
62
+ "multer": "^2.1.1",
58
63
  "nunjucks": "^3.2.4",
59
64
  "sharp": "^0.33.5",
65
+ "swup": "^4.8.3",
60
66
  "zod": "^3.23.0",
61
67
  "zod-to-json-schema": "^3.25.2"
62
68
  },
@@ -85,10 +91,10 @@
85
91
  }
86
92
  },
87
93
  "devDependencies": {
88
- "@types/express": "^4.17.21",
89
- "@types/node": "^20.14.0",
90
94
  "@faker-js/faker": "^9.9.0",
91
95
  "@playwright/test": "^1.48.0",
96
+ "@types/express": "^4.17.21",
97
+ "@types/node": "^20.14.0",
92
98
  "@vitest/coverage-v8": "^3.0.0",
93
99
  "better-sqlite3": "^11.10.0",
94
100
  "chokidar": "^3.5.3",
@@ -102,6 +108,7 @@
102
108
  "node": ">=18.0.0"
103
109
  },
104
110
  "overrides": {
111
+ "basic-ftp": "^5.3.0",
105
112
  "tar": "^7.5.11",
106
113
  "undici": "^6.24.0"
107
114
  }
@@ -308,8 +308,8 @@ function createApiHandlers(options) {
308
308
  } else if (value !== undefined && value !== null && value !== '') {
309
309
  switch (op) {
310
310
  case 'contains':
311
- // Apply LIKE for string/text types, or if type is unknown
312
- if (colType === 'string' || colType === 'text' || !colMeta) {
311
+ // Apply LIKE for string/text/file types, or if type is unknown
312
+ if (colType === 'string' || colType === 'text' || colType === 'file' || !colMeta) {
313
313
  query = query.where(colName, 'like', `%${value}%`);
314
314
  countQuery = countQuery.where(colName, 'like', `%${value}%`);
315
315
  }
@@ -319,13 +319,13 @@ function createApiHandlers(options) {
319
319
  countQuery = countQuery.where(colName, '=', value);
320
320
  break;
321
321
  case 'starts_with':
322
- if (colType === 'string' || colType === 'text' || !colMeta) {
322
+ if (colType === 'string' || colType === 'text' || colType === 'file' || !colMeta) {
323
323
  query = query.where(colName, 'like', `${value}%`);
324
324
  countQuery = countQuery.where(colName, 'like', `${value}%`);
325
325
  }
326
326
  break;
327
327
  case 'ends_with':
328
- if (colType === 'string' || colType === 'text' || !colMeta) {
328
+ if (colType === 'string' || colType === 'text' || colType === 'file' || !colMeta) {
329
329
  query = query.where(colName, 'like', `%${value}`);
330
330
  countQuery = countQuery.where(colName, 'like', `%${value}`);
331
331
  }
@@ -359,7 +359,7 @@ function createApiHandlers(options) {
359
359
  if (req.query.search) {
360
360
  const searchTerm = `%${req.query.search}%`;
361
361
  const stringColumns = Array.from(model.columns.entries())
362
- .filter(([_, meta]) => meta.type === 'string' || meta.type === 'text')
362
+ .filter(([_, meta]) => meta.type === 'string' || meta.type === 'text' || meta.type === 'file')
363
363
  .map(([name]) => name);
364
364
 
365
365
  if (stringColumns.length > 0) {