webspresso 0.0.58 → 0.0.60

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,8 @@
1
1
  # Webspresso
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/webspresso.svg?style=flat-square)](https://www.npmjs.com/package/webspresso)
4
+ [![vulnerabilities](https://npmx.dev/api/registry/badge/vulnerabilities/webspresso?style=shieldsio)](https://npmx.dev/package/webspresso)
5
+
3
6
  A minimal, file-based SSR framework for Node.js with Nunjucks templating.
4
7
 
5
8
  ## Features
@@ -12,7 +15,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
12
15
  - **Lifecycle Hooks**: Global and route-level hooks for request processing
13
16
  - **Template Helpers**: Laravel-inspired helper functions available in templates
14
17
  - **Plugin System**: Extensible architecture with version control and inter-plugin communication
15
- - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics
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
16
19
 
17
20
  ## Installation
18
21
 
@@ -1776,6 +1779,64 @@ const user = plugin.api.getModel('User'); // Single model
1776
1779
  const names = plugin.api.getModelNames(); // Model names
1777
1780
  ```
1778
1781
 
1782
+ ### Health check plugin
1783
+
1784
+ 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.
1785
+
1786
+ **Setup:**
1787
+
1788
+ ```javascript
1789
+ const { createApp, healthCheckPlugin } = require('webspresso');
1790
+
1791
+ const app = createApp({
1792
+ plugins: [
1793
+ healthCheckPlugin({
1794
+ path: '/health', // default
1795
+ verbose: true, // timestamp, uptime, NODE_ENV, framework name/version
1796
+ authorize: (req) => true, // optional — restrict who can read the endpoint
1797
+ checks: async ({ db }) => {
1798
+ if (db) await db.knex.raw('select 1');
1799
+ return { database: 'ok' };
1800
+ },
1801
+ }),
1802
+ ],
1803
+ });
1804
+ ```
1805
+
1806
+ - **`checks`**: If this function throws, the handler responds with **503** and `{ status: 'unhealthy', error, ... }`. Return a plain object to merge into `checks` on success (e.g. dependency status).
1807
+ - Use a **custom `path`** if your app already serves `GET /health` from `pages/`.
1808
+
1809
+ ### Swagger / OpenAPI plugin
1810
+
1811
+ Serves **OpenAPI 3** for file-based `pages/api` routes and optional [Zod](https://zod.dev) `schema` exports, plus a **Swagger UI** page. Defaults to **development only** (same idea as the schema explorer).
1812
+
1813
+ **Setup:**
1814
+
1815
+ ```javascript
1816
+ const { createApp, swaggerPlugin } = require('webspresso');
1817
+
1818
+ const app = createApp({
1819
+ plugins: [
1820
+ swaggerPlugin({
1821
+ path: '/_swagger', // UI: GET /_swagger, spec: GET /_swagger/openapi.json
1822
+ enabled: true, // default: true in development, false in production
1823
+ title: 'My API', // optional OpenAPI info.title
1824
+ serverUrl: 'https://api.example.com', // optional servers[0].url (else BASE_URL or localhost)
1825
+ includeOrmSchemas: false, // merge ORM model schemas into components.schemas
1826
+ ormExclude: ['Secret'], // when includeOrmSchemas is true
1827
+ authorize: (req) => true, // optional gate for both UI and JSON
1828
+ }),
1829
+ ],
1830
+ });
1831
+ ```
1832
+
1833
+ **Endpoints:**
1834
+
1835
+ - `GET /_swagger/openapi.json` — Full OpenAPI document (`paths` from API routes; request/response shapes from exported `schema({ z })` when present).
1836
+ - `GET /_swagger` — Swagger UI (loads the JSON above; requires network access for CDN assets).
1837
+
1838
+ In production, keep the plugin disabled or protect it with `authorize` / your own middleware.
1839
+
1779
1840
  ## Development
1780
1841
 
1781
1842
  ```bash
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Build OpenAPI 3 document from file-router API route metadata + Zod schemas
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { zodToJsonSchema } = require('zod-to-json-schema');
8
+ const { compileSchema } = require('../compileSchema');
9
+ const { generateOrmOpenApiSchemas } = require('./orm-components');
10
+
11
+ /**
12
+ * Express route pattern → OpenAPI path (e.g. /users/:id → /users/{id})
13
+ * @param {string} expressPath
14
+ * @returns {string}
15
+ */
16
+ function expressPathToOpenApi(expressPath) {
17
+ return String(expressPath)
18
+ .replace(/:([A-Za-z0-9_]+)/g, '{$1}')
19
+ .replace(/\*/g, '{wildcard}');
20
+ }
21
+
22
+ /**
23
+ * @param {import('zod').ZodObject<any>} zodObj
24
+ * @param {'path' | 'query'} where
25
+ * @returns {object[]}
26
+ */
27
+ function expandZodObjectToParameters(zodObj, where) {
28
+ if (!zodObj || zodObj._def?.typeName !== 'ZodObject') return [];
29
+ const shape = zodObj.shape;
30
+ return Object.entries(shape).map(([name, zType]) => ({
31
+ name,
32
+ in: where,
33
+ required: where === 'path' ? true : !zType.isOptional(),
34
+ schema: zodToJsonSchema(zType, { target: 'openApi3', $refStrategy: 'none' }),
35
+ }));
36
+ }
37
+
38
+ /**
39
+ * @param {object} route - { type, method, pattern, file }
40
+ * @param {object|null} compiled - compileSchema result
41
+ * @returns {object} OpenAPI Operation Object
42
+ */
43
+ function buildOperation(route, compiled) {
44
+ const filePath = route.file;
45
+ const method = route.method.toLowerCase();
46
+ const summary = `${method.toUpperCase()} ${filePath}`;
47
+
48
+ const op = {
49
+ tags: ['api'],
50
+ summary,
51
+ operationId: String(filePath)
52
+ .replace(/[^a-zA-Z0-9]+/g, '_')
53
+ .replace(/^_|_$/g, ''),
54
+ responses: {
55
+ 200: {
56
+ description: 'OK',
57
+ },
58
+ },
59
+ };
60
+
61
+ if (compiled?.response) {
62
+ op.responses['200'] = {
63
+ description: 'OK',
64
+ content: {
65
+ 'application/json': {
66
+ schema: zodToJsonSchema(compiled.response, { target: 'openApi3', $refStrategy: 'none' }),
67
+ },
68
+ },
69
+ };
70
+ }
71
+
72
+ const parameters = [
73
+ ...expandZodObjectToParameters(compiled?.params, 'path'),
74
+ ...expandZodObjectToParameters(compiled?.query, 'query'),
75
+ ];
76
+
77
+ if (parameters.length) {
78
+ op.parameters = parameters;
79
+ }
80
+
81
+ if (compiled?.body) {
82
+ op.requestBody = {
83
+ content: {
84
+ 'application/json': {
85
+ schema: zodToJsonSchema(compiled.body, { target: 'openApi3', $refStrategy: 'none' }),
86
+ },
87
+ },
88
+ };
89
+ }
90
+
91
+ return op;
92
+ }
93
+
94
+ /**
95
+ * @param {object} opts
96
+ * @param {object[]} opts.routes - route metadata from mountPages
97
+ * @param {string} opts.pagesDir
98
+ * @param {boolean} [opts.includeOrmSchemas]
99
+ * @param {string[]} [opts.ormExclude]
100
+ * @param {object} [opts.info]
101
+ * @param {object[]} [opts.servers]
102
+ * @returns {object} OpenAPI 3.0.x document
103
+ */
104
+ function buildOpenApiDocument(opts) {
105
+ const {
106
+ routes = [],
107
+ pagesDir,
108
+ includeOrmSchemas = false,
109
+ ormExclude = [],
110
+ info = {},
111
+ servers = [{ url: '/' }],
112
+ } = opts;
113
+
114
+ if (!pagesDir) {
115
+ throw new Error('buildOpenApiDocument: pagesDir is required');
116
+ }
117
+
118
+ const paths = {};
119
+ const apiRoutes = routes.filter((r) => r.type === 'api');
120
+ const absPages = path.resolve(pagesDir);
121
+
122
+ for (const route of apiRoutes) {
123
+ const fullPath = path.join(absPages, route.file);
124
+ if (!fs.existsSync(fullPath)) {
125
+ continue;
126
+ }
127
+
128
+ let mod;
129
+ try {
130
+ mod = require(fullPath);
131
+ } catch {
132
+ continue;
133
+ }
134
+
135
+ let compiled = null;
136
+ try {
137
+ compiled = compileSchema(fullPath, mod);
138
+ } catch {
139
+ compiled = null;
140
+ }
141
+
142
+ const openApiPath = expressPathToOpenApi(route.pattern);
143
+ const method = route.method.toLowerCase();
144
+
145
+ if (!paths[openApiPath]) {
146
+ paths[openApiPath] = {};
147
+ }
148
+
149
+ paths[openApiPath][method] = buildOperation(route, compiled);
150
+ }
151
+
152
+ const doc = {
153
+ openapi: '3.0.3',
154
+ info: {
155
+ title: info.title || 'API',
156
+ version: info.version || '1.0.0',
157
+ ...(info.description ? { description: info.description } : {}),
158
+ },
159
+ servers,
160
+ paths,
161
+ components: {
162
+ schemas: includeOrmSchemas ? generateOrmOpenApiSchemas({ exclude: ormExclude }) : {},
163
+ },
164
+ };
165
+
166
+ return doc;
167
+ }
168
+
169
+ module.exports = {
170
+ expressPathToOpenApi,
171
+ buildOpenApiDocument,
172
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * ORM column → OpenAPI schema fragments (shared by schema-explorer and swagger plugin)
3
+ */
4
+
5
+ const { getAllModels } = require('../orm/model');
6
+
7
+ /**
8
+ * Generate OpenAPI 3 components.schemas from registered ORM models
9
+ * @param {{ exclude?: string[] }} options
10
+ * @returns {Record<string, object>}
11
+ */
12
+ function generateOrmOpenApiSchemas(options = {}) {
13
+ const { exclude = [] } = options;
14
+ const models = getAllModels();
15
+ const schemas = {};
16
+
17
+ for (const [name, model] of models) {
18
+ if (exclude.includes(name)) continue;
19
+
20
+ const properties = {};
21
+ const required = [];
22
+
23
+ if (model.columns) {
24
+ for (const [colName, meta] of model.columns) {
25
+ properties[colName] = columnToOpenApiType(meta);
26
+
27
+ if (!meta.nullable && !meta.autoIncrement && meta.default === undefined) {
28
+ required.push(colName);
29
+ }
30
+ }
31
+ }
32
+
33
+ schemas[name] = {
34
+ type: 'object',
35
+ properties,
36
+ required: required.length > 0 ? required : undefined,
37
+ };
38
+
39
+ const inputProperties = {};
40
+ const inputRequired = [];
41
+
42
+ if (model.columns) {
43
+ for (const [colName, meta] of model.columns) {
44
+ if (meta.autoIncrement || meta.auto) continue;
45
+
46
+ inputProperties[colName] = columnToOpenApiType(meta);
47
+
48
+ if (!meta.nullable && meta.default === undefined) {
49
+ inputRequired.push(colName);
50
+ }
51
+ }
52
+ }
53
+
54
+ schemas[`${name}Input`] = {
55
+ type: 'object',
56
+ properties: inputProperties,
57
+ required: inputRequired.length > 0 ? inputRequired : undefined,
58
+ };
59
+ }
60
+
61
+ return schemas;
62
+ }
63
+
64
+ /**
65
+ * @param {Object} meta - Column metadata
66
+ * @returns {Object}
67
+ */
68
+ function columnToOpenApiType(meta) {
69
+ const result = {};
70
+
71
+ switch (meta.type) {
72
+ case 'bigint':
73
+ case 'integer':
74
+ result.type = 'integer';
75
+ if (meta.type === 'bigint') result.format = 'int64';
76
+ break;
77
+
78
+ case 'float':
79
+ case 'decimal':
80
+ result.type = 'number';
81
+ if (meta.type === 'decimal') result.format = 'double';
82
+ break;
83
+
84
+ case 'boolean':
85
+ result.type = 'boolean';
86
+ break;
87
+
88
+ case 'date':
89
+ result.type = 'string';
90
+ result.format = 'date';
91
+ break;
92
+
93
+ case 'datetime':
94
+ case 'timestamp':
95
+ result.type = 'string';
96
+ result.format = 'date-time';
97
+ break;
98
+
99
+ case 'uuid':
100
+ result.type = 'string';
101
+ result.format = 'uuid';
102
+ break;
103
+
104
+ case 'json':
105
+ result.type = 'object';
106
+ break;
107
+
108
+ case 'enum':
109
+ result.type = 'string';
110
+ if (meta.enumValues) {
111
+ result.enum = meta.enumValues;
112
+ }
113
+ break;
114
+
115
+ case 'text':
116
+ case 'string':
117
+ default:
118
+ result.type = 'string';
119
+ if (meta.maxLength) {
120
+ result.maxLength = meta.maxLength;
121
+ }
122
+ break;
123
+ }
124
+
125
+ if (meta.nullable) {
126
+ result.nullable = true;
127
+ }
128
+
129
+ if (meta.default !== undefined) {
130
+ result.default = meta.default;
131
+ }
132
+
133
+ return result;
134
+ }
135
+
136
+ module.exports = {
137
+ generateOrmOpenApiSchemas,
138
+ columnToOpenApiType,
139
+ };
package/index.js CHANGED
@@ -30,7 +30,7 @@ const {
30
30
  const orm = require('./core/orm');
31
31
 
32
32
  // Built-in plugins
33
- const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin } = require('./plugins');
33
+ const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin, swaggerPlugin, healthCheckPlugin } = require('./plugins');
34
34
 
35
35
  module.exports = {
36
36
  // Main API
@@ -72,4 +72,6 @@ module.exports = {
72
72
  siteAnalyticsPlugin,
73
73
  auditLogPlugin,
74
74
  recaptchaPlugin,
75
+ swaggerPlugin,
76
+ healthCheckPlugin,
75
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
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
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "license": "MIT",
31
31
  "repository": {
32
32
  "type": "git",
33
- "url": "https://github.com/everytools/webspresso.git"
33
+ "url": "https://github.com/litepacks/webspresso.git"
34
34
  },
35
35
  "files": [
36
36
  "index.js",
@@ -54,7 +54,8 @@
54
54
  "knex": "^3.1.0",
55
55
  "nunjucks": "^3.2.4",
56
56
  "sharp": "^0.33.5",
57
- "zod": "^3.23.0"
57
+ "zod": "^3.23.0",
58
+ "zod-to-json-schema": "^3.25.2"
58
59
  },
59
60
  "peerDependencies": {
60
61
  "@faker-js/faker": ">=8.0.0",
@@ -83,15 +84,19 @@
83
84
  "devDependencies": {
84
85
  "@faker-js/faker": "^9.9.0",
85
86
  "@playwright/test": "^1.48.0",
86
- "@vitest/coverage-v8": "^1.2.0",
87
+ "@vitest/coverage-v8": "^3.0.0",
87
88
  "better-sqlite3": "^11.10.0",
88
89
  "chokidar": "^3.5.3",
89
90
  "dotenv": "^16.3.1",
90
- "release-it": "^17.11.0",
91
+ "release-it": "^19.0.0",
91
92
  "supertest": "^6.3.4",
92
- "vitest": "^1.2.0"
93
+ "vitest": "^3.0.0"
93
94
  },
94
95
  "engines": {
95
96
  "node": ">=18.0.0"
97
+ },
98
+ "overrides": {
99
+ "tar": "^7.5.11",
100
+ "undici": "^6.24.0"
96
101
  }
97
102
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Health check plugin — liveness/readiness style HTTP endpoint for load balancers and monitoring
3
+ */
4
+
5
+ const pathMod = require('path');
6
+ const PKG = require(pathMod.join(__dirname, '..', 'package.json'));
7
+
8
+ /**
9
+ * @param {Object} [options]
10
+ * @param {string} [options.path='/health'] — GET endpoint path
11
+ * @param {boolean} [options.enabled=true] — Disable entirely when false
12
+ * @param {function} [options.authorize] — (req) => boolean; optional gate (e.g. internal network only)
13
+ * @param {boolean} [options.verbose=true] — Include timestamp, uptime, env, framework version
14
+ * @param {function} [options.checks] — async ({ req, db, options }) => Record<string, string> — custom checks; thrown error → 503
15
+ */
16
+ function healthCheckPlugin(options = {}) {
17
+ const {
18
+ path: routePath = '/health',
19
+ enabled,
20
+ authorize,
21
+ verbose = true,
22
+ checks,
23
+ } = options;
24
+
25
+ const isEnabled = enabled !== undefined ? enabled : true;
26
+ const normalizedPath = routePath.startsWith('/') ? routePath : `/${routePath}`;
27
+
28
+ return {
29
+ name: 'health-check',
30
+ version: '1.0.0',
31
+ description: 'HTTP health endpoint for probes and monitoring',
32
+
33
+ onRoutesReady(ctx) {
34
+ if (!isEnabled) {
35
+ return;
36
+ }
37
+
38
+ ctx.addRoute('get', normalizedPath, async (req, res) => {
39
+ if (authorize && !authorize(req)) {
40
+ return res.status(403).json({ error: 'Forbidden' });
41
+ }
42
+
43
+ const body = {
44
+ status: 'ok',
45
+ };
46
+
47
+ if (verbose) {
48
+ body.timestamp = new Date().toISOString();
49
+ body.uptime = process.uptime();
50
+ body.env = process.env.NODE_ENV || 'development';
51
+ body.framework = {
52
+ name: PKG.name,
53
+ version: PKG.version,
54
+ };
55
+ }
56
+
57
+ if (typeof checks === 'function') {
58
+ try {
59
+ const result = await checks({
60
+ req,
61
+ db: ctx.db,
62
+ options: ctx.options,
63
+ });
64
+ if (result && typeof result === 'object') {
65
+ body.checks = result;
66
+ }
67
+ } catch (err) {
68
+ body.status = 'unhealthy';
69
+ body.error = err.message || String(err);
70
+ return res.status(503).json(body);
71
+ }
72
+ }
73
+
74
+ res.json(body);
75
+ });
76
+
77
+ if (process.env.NODE_ENV !== 'production') {
78
+ console.log(` Health check: GET ${normalizedPath}`);
79
+ }
80
+ },
81
+ };
82
+ }
83
+
84
+ module.exports = healthCheckPlugin;
package/plugins/index.js CHANGED
@@ -12,6 +12,8 @@ const seoCheckerPlugin = require('./seo-checker');
12
12
  const siteAnalyticsPlugin = require('./site-analytics');
13
13
  const auditLogPlugin = require('./audit-log');
14
14
  const recaptchaPlugin = require('./recaptcha');
15
+ const swaggerPlugin = require('./swagger');
16
+ const healthCheckPlugin = require('./health-check');
15
17
 
16
18
  module.exports = {
17
19
  sitemapPlugin,
@@ -23,5 +25,7 @@ module.exports = {
23
25
  siteAnalyticsPlugin,
24
26
  auditLogPlugin,
25
27
  recaptchaPlugin,
28
+ swaggerPlugin,
29
+ healthCheckPlugin,
26
30
  };
27
31
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { getAllModels, getModel } = require('../core/orm/model');
8
8
  const { getColumnMeta } = require('../core/orm/schema-helpers');
9
+ const { generateOrmOpenApiSchemas } = require('../core/openapi/orm-components');
9
10
 
10
11
  /**
11
12
  * Create Schema Explorer plugin
@@ -99,7 +100,7 @@ function schemaExplorerPlugin(options = {}) {
99
100
  return res.status(403).json({ error: 'Forbidden' });
100
101
  }
101
102
 
102
- const openApiSchemas = generateOpenApiSchemas({ exclude });
103
+ const openApiSchemas = generateOrmOpenApiSchemas({ exclude });
103
104
 
104
105
  res.json({
105
106
  openapi: '3.0.0',
@@ -262,137 +263,5 @@ function serializeRelations(relations) {
262
263
  return result;
263
264
  }
264
265
 
265
- /**
266
- * Generate OpenAPI schemas from models
267
- * @param {Object} options
268
- * @returns {Object}
269
- */
270
- function generateOpenApiSchemas(options) {
271
- const { exclude } = options;
272
- const models = getAllModels();
273
- const schemas = {};
274
-
275
- for (const [name, model] of models) {
276
- if (exclude.includes(name)) continue;
277
-
278
- const properties = {};
279
- const required = [];
280
-
281
- if (model.columns) {
282
- for (const [colName, meta] of model.columns) {
283
- properties[colName] = columnToOpenApiType(meta);
284
-
285
- if (!meta.nullable && !meta.autoIncrement && meta.default === undefined) {
286
- required.push(colName);
287
- }
288
- }
289
- }
290
-
291
- schemas[name] = {
292
- type: 'object',
293
- properties,
294
- required: required.length > 0 ? required : undefined,
295
- };
296
-
297
- // Add input schema (without auto fields)
298
- const inputProperties = {};
299
- const inputRequired = [];
300
-
301
- if (model.columns) {
302
- for (const [colName, meta] of model.columns) {
303
- // Skip auto-generated fields for input
304
- if (meta.autoIncrement || meta.auto) continue;
305
-
306
- inputProperties[colName] = columnToOpenApiType(meta);
307
-
308
- if (!meta.nullable && meta.default === undefined) {
309
- inputRequired.push(colName);
310
- }
311
- }
312
- }
313
-
314
- schemas[`${name}Input`] = {
315
- type: 'object',
316
- properties: inputProperties,
317
- required: inputRequired.length > 0 ? inputRequired : undefined,
318
- };
319
- }
320
-
321
- return schemas;
322
- }
323
-
324
- /**
325
- * Convert column metadata to OpenAPI type
326
- * @param {Object} meta - Column metadata
327
- * @returns {Object}
328
- */
329
- function columnToOpenApiType(meta) {
330
- const result = {};
331
-
332
- switch (meta.type) {
333
- case 'bigint':
334
- case 'integer':
335
- result.type = 'integer';
336
- if (meta.type === 'bigint') result.format = 'int64';
337
- break;
338
-
339
- case 'float':
340
- case 'decimal':
341
- result.type = 'number';
342
- if (meta.type === 'decimal') result.format = 'double';
343
- break;
344
-
345
- case 'boolean':
346
- result.type = 'boolean';
347
- break;
348
-
349
- case 'date':
350
- result.type = 'string';
351
- result.format = 'date';
352
- break;
353
-
354
- case 'datetime':
355
- case 'timestamp':
356
- result.type = 'string';
357
- result.format = 'date-time';
358
- break;
359
-
360
- case 'uuid':
361
- result.type = 'string';
362
- result.format = 'uuid';
363
- break;
364
-
365
- case 'json':
366
- result.type = 'object';
367
- break;
368
-
369
- case 'enum':
370
- result.type = 'string';
371
- if (meta.enumValues) {
372
- result.enum = meta.enumValues;
373
- }
374
- break;
375
-
376
- case 'text':
377
- case 'string':
378
- default:
379
- result.type = 'string';
380
- if (meta.maxLength) {
381
- result.maxLength = meta.maxLength;
382
- }
383
- break;
384
- }
385
-
386
- if (meta.nullable) {
387
- result.nullable = true;
388
- }
389
-
390
- if (meta.default !== undefined) {
391
- result.default = meta.default;
392
- }
393
-
394
- return result;
395
- }
396
-
397
266
  module.exports = schemaExplorerPlugin;
398
267
 
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Swagger / OpenAPI plugin — HTTP API docs from file-based routes + Zod, optional ORM schemas
3
+ */
4
+
5
+ const path = require('path');
6
+ const { buildOpenApiDocument } = require('../core/openapi/build-from-api-routes');
7
+
8
+ const PKG = require(path.join(__dirname, '..', 'package.json'));
9
+
10
+ /**
11
+ * @param {string} specUrl - Absolute path on same origin (e.g. /_swagger/openapi.json)
12
+ */
13
+ function buildSwaggerUiHtml(specUrl) {
14
+ const urlJson = JSON.stringify(specUrl);
15
+ return `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>API documentation</title>
21
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" crossorigin />
22
+ </head>
23
+ <body>
24
+ <div id="swagger-ui"></div>
25
+ <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
26
+ <script>
27
+ window.onload = function () {
28
+ SwaggerUIBundle({
29
+ url: ${urlJson},
30
+ dom_id: '#swagger-ui',
31
+ deepLinking: true
32
+ });
33
+ };
34
+ </script>
35
+ </body>
36
+ </html>`;
37
+ }
38
+
39
+ /**
40
+ * @param {Object} [options]
41
+ * @param {string} [options.path='/_swagger'] - Base path; JSON at `{path}/openapi.json`, UI at `{path}`
42
+ * @param {boolean} [options.enabled] - Default: development only
43
+ * @param {Function} [options.authorize] - (req) => boolean
44
+ * @param {boolean} [options.includeOrmSchemas] - Merge ORM components.schemas
45
+ * @param {string[]} [options.ormExclude] - Model names to exclude when includeOrmSchemas
46
+ * @param {string} [options.title] - OpenAPI info.title
47
+ * @param {string} [options.version] - OpenAPI info.version
48
+ * @param {string} [options.description] - OpenAPI info.description
49
+ * @param {string} [options.serverUrl] - Override servers[0].url (default: BASE_URL or http://localhost:3000)
50
+ */
51
+ function swaggerPlugin(options = {}) {
52
+ const {
53
+ path: basePath = '/_swagger',
54
+ enabled,
55
+ authorize,
56
+ includeOrmSchemas = false,
57
+ ormExclude = [],
58
+ title,
59
+ version,
60
+ description,
61
+ serverUrl,
62
+ } = options;
63
+
64
+ const normalizedBase = `/${String(basePath).replace(/^\/+|\/+$/g, '')}`;
65
+ const jsonPath = `${normalizedBase}/openapi.json`;
66
+ const uiPath = normalizedBase;
67
+
68
+ return {
69
+ name: 'swagger',
70
+ version: '1.0.0',
71
+
72
+ onRoutesReady(ctx) {
73
+ const isDev = process.env.NODE_ENV !== 'production';
74
+ const isEnabled = enabled !== undefined ? enabled : isDev;
75
+
76
+ if (!isEnabled) {
77
+ return;
78
+ }
79
+
80
+ const forbid = (req, res) => {
81
+ if (authorize && !authorize(req)) {
82
+ res.status(403).json({ error: 'Forbidden' });
83
+ return true;
84
+ }
85
+ return false;
86
+ };
87
+
88
+ const server = serverUrl || process.env.BASE_URL || 'http://localhost:3000';
89
+
90
+ ctx.addRoute('get', jsonPath, (req, res) => {
91
+ if (forbid(req, res)) return;
92
+ try {
93
+ const doc = buildOpenApiDocument({
94
+ routes: ctx.routes || [],
95
+ pagesDir: ctx.options.pagesDir,
96
+ includeOrmSchemas,
97
+ ormExclude,
98
+ info: {
99
+ title: title || PKG.name || 'API',
100
+ version: version || PKG.version || '1.0.0',
101
+ description:
102
+ description ||
103
+ 'Generated from pages/api routes and optional Zod schema exports.',
104
+ },
105
+ servers: [{ url: server.replace(/\/$/, '') }],
106
+ });
107
+ res.json(doc);
108
+ } catch (err) {
109
+ console.warn('[swagger] OpenAPI generation failed:', err.message);
110
+ res.status(500).json({ error: 'OpenAPI generation failed', message: err.message });
111
+ }
112
+ });
113
+
114
+ ctx.addRoute('get', uiPath, (req, res) => {
115
+ if (forbid(req, res)) return;
116
+ res.type('html').send(buildSwaggerUiHtml(jsonPath));
117
+ });
118
+
119
+ if (isDev) {
120
+ console.log(` Swagger UI: ${uiPath} (OpenAPI: ${jsonPath})`);
121
+ }
122
+ },
123
+ };
124
+ }
125
+
126
+ module.exports = swaggerPlugin;