webspresso 0.0.104 → 0.0.106

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.
Files changed (233) hide show
  1. package/.agents/AGENTS.md +21 -2
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +220 -8
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/FLAGS.md +118 -0
  7. package/.agents/HEALTH.md +118 -0
  8. package/.agents/LIMITS.md +109 -0
  9. package/.agents/LOGGING.md +93 -0
  10. package/.agents/METRICS.md +127 -0
  11. package/.agents/MIGRATION_JOBS.md +95 -0
  12. package/.agents/ORM.md +155 -1
  13. package/.agents/PLUGINS.md +38 -3
  14. package/.agents/QUEUE.md +42 -0
  15. package/.agents/RATE_LIMIT.md +147 -0
  16. package/.agents/RESILIENCE.md +235 -0
  17. package/.agents/SCALE_OUT.md +109 -0
  18. package/.agents/SECRETS.md +117 -0
  19. package/AGENTS.md +16 -0
  20. package/README.md +2 -2
  21. package/bin/commands/add-aws.js +396 -0
  22. package/bin/commands/add-ci.js +523 -0
  23. package/bin/commands/add-cloudflare.js +240 -0
  24. package/bin/commands/add-cloudrun.js +381 -0
  25. package/bin/commands/add-docker.js +6 -6
  26. package/bin/commands/add-migration-job.js +396 -0
  27. package/bin/commands/assets-sync.js +209 -0
  28. package/bin/commands/client-generate.js +52 -0
  29. package/bin/commands/console.js +53 -0
  30. package/bin/commands/db-migrate.js +47 -2
  31. package/bin/commands/db-studio.js +84 -0
  32. package/bin/commands/deploy-check.js +242 -13
  33. package/bin/commands/dev.js +1 -1
  34. package/bin/commands/export.js +87 -0
  35. package/bin/commands/new.js +5 -5
  36. package/bin/commands/openapi.js +1 -1
  37. package/bin/commands/secrets-pull.js +208 -0
  38. package/bin/commands/start.js +1 -1
  39. package/bin/utils/db.js +7 -1
  40. package/bin/utils/project.js +2 -2
  41. package/bin/webspresso.js +22 -0
  42. package/core/app.js +421 -0
  43. package/core/auth/authenticate.js +93 -0
  44. package/core/auth/hash.js +5 -4
  45. package/core/auth/index.js +34 -4
  46. package/core/auth/jwt.js +5 -54
  47. package/core/auth/magic-link.js +4 -5
  48. package/core/auth/manager.js +53 -10
  49. package/core/auth/middleware.js +16 -68
  50. package/core/auth/oauth.js +8 -17
  51. package/core/auth/redis-session.js +1008 -0
  52. package/core/auth/sessions.js +156 -0
  53. package/core/auth/totp.js +2 -1
  54. package/core/cache/byte-size.js +123 -0
  55. package/core/cache/cdn.js +296 -0
  56. package/core/cache/index.js +33 -0
  57. package/core/cache/lru.js +477 -0
  58. package/core/cache/tagged-lru.js +354 -0
  59. package/core/commands/registry.js +6 -20
  60. package/core/console/commands.js +148 -0
  61. package/core/console/context.js +260 -0
  62. package/core/console/eval.js +70 -0
  63. package/core/console/index.js +22 -0
  64. package/core/console/repl.js +102 -0
  65. package/core/early-hints/index.js +267 -0
  66. package/core/errors/domain.js +49 -0
  67. package/core/errors/http.js +31 -0
  68. package/core/errors/index.js +10 -0
  69. package/core/errors/normalize.js +88 -2
  70. package/core/events/discovery.js +6 -24
  71. package/core/events/dispatcher.js +9 -7
  72. package/core/events/signal.js +7 -0
  73. package/core/export/discovery.js +191 -0
  74. package/core/export/exporter.js +277 -0
  75. package/core/export/index.js +24 -0
  76. package/core/flags/evaluator.js +252 -0
  77. package/core/flags/hasher.js +37 -0
  78. package/core/flags/index.js +21 -0
  79. package/core/flags/manager.js +263 -0
  80. package/core/flags/store.js +126 -0
  81. package/core/health/index.js +41 -0
  82. package/core/health/middleware.js +213 -0
  83. package/core/health/normalize.js +140 -0
  84. package/core/health/probes.js +203 -0
  85. package/core/http/adapter.js +74 -0
  86. package/core/http/express-adapter.js +46 -0
  87. package/core/http/hono-adapter.js +531 -0
  88. package/core/http/index.js +41 -0
  89. package/core/http/pipeline.js +485 -0
  90. package/core/index.js +336 -0
  91. package/core/limits/index.js +34 -0
  92. package/core/limits/middleware.js +325 -0
  93. package/core/limits/normalize.js +222 -0
  94. package/core/logger/formatters.js +264 -0
  95. package/core/logger/index.js +260 -0
  96. package/core/logger/trace.js +137 -0
  97. package/core/metrics/collectors.js +125 -0
  98. package/core/metrics/index.js +45 -0
  99. package/core/metrics/middleware.js +230 -0
  100. package/core/metrics/registry.js +366 -0
  101. package/core/orm/batch.js +70 -0
  102. package/core/orm/cache/fingerprint.js +2 -2
  103. package/core/orm/cache/memory-provider.js +27 -110
  104. package/core/orm/d1.js +265 -0
  105. package/core/orm/index.js +176 -13
  106. package/core/orm/json-fields.js +22 -8
  107. package/core/orm/migrate-cloud.js +194 -0
  108. package/core/orm/model.js +10 -0
  109. package/core/orm/pool.js +97 -0
  110. package/core/orm/query-builder.js +72 -9
  111. package/core/orm/repository.js +318 -19
  112. package/core/orm/schema-helpers.js +8 -10
  113. package/core/orm/scopes.js +321 -1
  114. package/core/orm/transaction.js +10 -2
  115. package/core/orm/utils.js +4 -86
  116. package/core/queue/adapters/cloudflare.js +131 -0
  117. package/core/queue/adapters/database.js +4 -8
  118. package/core/queue/adapters/pubsub.js +400 -0
  119. package/core/queue/adapters/sqs.js +419 -0
  120. package/core/queue/adapters/wait-until.js +77 -0
  121. package/core/queue/index.js +8 -0
  122. package/core/queue/manager.js +66 -41
  123. package/core/rate-limit/index.js +22 -0
  124. package/core/rate-limit/limiter.js +219 -0
  125. package/core/rate-limit/stores/memory.js +129 -0
  126. package/core/rate-limit/stores/redis.js +151 -0
  127. package/core/rate-limit/utils.js +112 -0
  128. package/core/resilience/circuit-breaker.js +422 -0
  129. package/core/resilience/errors.js +63 -0
  130. package/core/resilience/index.js +53 -0
  131. package/core/resilience/registry.js +97 -0
  132. package/core/resilience/retry.js +203 -0
  133. package/core/schema-cache.js +9 -2
  134. package/core/secrets/index.js +113 -0
  135. package/core/secrets/manager.js +308 -0
  136. package/core/secrets/providers/aws.js +288 -0
  137. package/core/secrets/providers/env.js +44 -0
  138. package/core/secrets/providers/gcp.js +307 -0
  139. package/core/secrets/providers/memory.js +76 -0
  140. package/core/shutdown/http-adapter.js +4 -1
  141. package/core/shutdown/shutdown-manager.js +129 -19
  142. package/core/stream/index.js +19 -0
  143. package/core/stream/text-stream.js +326 -0
  144. package/core/studio/index.js +43 -0
  145. package/core/studio/inspector.js +258 -0
  146. package/core/studio/server.js +325 -0
  147. package/core/studio/ui.js +681 -0
  148. package/core/utils/array.js +12 -0
  149. package/core/utils/base64url.js +8 -0
  150. package/core/utils/crypto.js +118 -0
  151. package/core/utils/ensure-leading-slash.js +6 -0
  152. package/core/utils/escape-regexp.js +7 -0
  153. package/core/utils/fetch.js +6 -5
  154. package/core/utils/fs.js +144 -0
  155. package/core/utils/hmac.js +6 -0
  156. package/core/utils/json.js +98 -0
  157. package/core/utils/object.js +106 -0
  158. package/core/utils/random.js +9 -0
  159. package/core/utils/regex.js +77 -0
  160. package/core/utils/sha256.js +6 -0
  161. package/core/utils/string.js +87 -0
  162. package/core/utils/strip-trailing-slash.js +6 -0
  163. package/core/utils/time.js +96 -0
  164. package/core/utils/timeout.js +348 -0
  165. package/core/utils/url.js +106 -0
  166. package/core/utils/walk.js +7 -0
  167. package/index.d.ts +1549 -2
  168. package/index.js +272 -2
  169. package/package.json +3 -2
  170. package/plugins/admin-panel/core/api-extensions.js +2 -5
  171. package/plugins/admin-panel/index.js +13 -6
  172. package/plugins/admin-panel/modules/menu.js +1 -1
  173. package/plugins/api-key/crypto.js +3 -3
  174. package/plugins/api-key/index.js +3 -2
  175. package/plugins/api-key/scopes.js +5 -7
  176. package/plugins/audit-log/api-handlers.js +6 -15
  177. package/plugins/audit-log/index.js +3 -1
  178. package/plugins/audit-log/parse.js +3 -3
  179. package/plugins/basic-auth/index.js +3 -2
  180. package/plugins/cors.js +3 -2
  181. package/plugins/csrf/index.js +9 -12
  182. package/plugins/csv/index.js +8 -5
  183. package/plugins/dashboard/app.js +2 -2
  184. package/plugins/dashboard/index.js +5 -5
  185. package/plugins/data-exchange/record-selection.js +2 -5
  186. package/plugins/email/api-handlers.js +5 -18
  187. package/plugins/email/auth-bridge.js +3 -2
  188. package/plugins/email/index.js +11 -9
  189. package/plugins/health-check.js +2 -1
  190. package/plugins/index.js +5 -0
  191. package/plugins/lazy.js +5 -6
  192. package/plugins/mcp/discovery.js +18 -17
  193. package/plugins/polar/src/api.js +2 -1
  194. package/plugins/polar/src/webhooks.js +5 -6
  195. package/plugins/queue/index.js +18 -2
  196. package/plugins/rate-limit/index.js +3 -1
  197. package/plugins/redirect/index.js +16 -9
  198. package/plugins/rls/index.js +170 -0
  199. package/plugins/seo-checker/index.js +2 -2
  200. package/plugins/site-analytics/index.js +3 -1
  201. package/plugins/site-analytics/tracking.js +2 -2
  202. package/plugins/sitemap.js +3 -2
  203. package/plugins/swagger.js +12 -5
  204. package/plugins/upload/gcs-provider.js +148 -0
  205. package/plugins/upload/index.js +136 -77
  206. package/plugins/upload/orphan-pruner.js +4 -3
  207. package/plugins/upload/s3-provider.js +257 -0
  208. package/plugins/webhooks/crypto.js +6 -11
  209. package/plugins/webhooks/dispatcher.js +8 -8
  210. package/plugins/webhooks/index.js +33 -23
  211. package/plugins/webhooks/matcher.js +7 -11
  212. package/plugins/webhooks/sender.js +8 -11
  213. package/plugins/xlsx/index.js +7 -5
  214. package/src/api/api-loader.js +12 -95
  215. package/src/api/define-api.js +1 -0
  216. package/src/app-context.js +41 -5
  217. package/src/client-runtime/constants.js +10 -0
  218. package/src/client-runtime/mount.js +2 -3
  219. package/src/dev/banner.js +6 -2
  220. package/src/dev/client-gen.js +458 -0
  221. package/src/discovery/scan-routes.js +5 -2
  222. package/src/file-router.js +3 -3
  223. package/src/helpers.js +212 -24
  224. package/src/modules/module-discovery.js +1 -9
  225. package/src/pages/page-loader.js +13 -154
  226. package/src/plugin-manager.js +99 -18
  227. package/src/server.js +335 -133
  228. package/src/services/discovery.js +19 -40
  229. package/src/services/executor.js +95 -25
  230. package/src/services/memoize.js +3 -25
  231. package/src/services/registry.js +15 -1
  232. package/src/services/validator.js +38 -20
  233. package/src/template/precompile.js +8 -19
@@ -0,0 +1,458 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Type-Safe Frontend Client Generator
5
+ * Generates an end-to-end typed TypeScript HTTP client from API routes and Zod schemas
6
+ * @module src/dev/client-gen
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { scanRoutes } = require('../discovery/scan-routes');
12
+ const { zodToTsType } = require('./typegen');
13
+ const { z } = require('../../core/validation');
14
+
15
+ /**
16
+ * Normalizes a URL path into clean segments, stripping /api prefix if desired
17
+ * @param {string} routePattern e.g. "/api/notes/:id"
18
+ * @returns {string[]} e.g. ['api', 'notes', ':id']
19
+ */
20
+ function pathToSegments(routePattern) {
21
+ return routePattern
22
+ .replace(/^\/+|\/+$/g, '')
23
+ .split('/')
24
+ .filter(Boolean);
25
+ }
26
+
27
+ /**
28
+ * Inspects a route file and extracts schemas
29
+ * @param {string} filePath
30
+ * @returns {{ bodyType: string, queryType: string, paramsType: string, responseType: string }}
31
+ */
32
+ function extractRouteTypes(filePath) {
33
+ let bodyType = 'unknown';
34
+ let queryType = 'Record<string, unknown>';
35
+ let paramsType = 'Record<string, string>';
36
+ let responseType = 'any';
37
+
38
+ try {
39
+ const fullPath = path.resolve(filePath);
40
+ if (!fs.existsSync(fullPath)) return { bodyType, queryType, paramsType, responseType };
41
+
42
+ delete require.cache[fullPath];
43
+ const mod = require(fullPath);
44
+ const def = mod && mod.default ? mod.default : mod;
45
+
46
+ let schema = def && def.schema;
47
+ if (typeof schema === 'function') {
48
+ try {
49
+ schema = schema({ z });
50
+ } catch {
51
+ schema = null;
52
+ }
53
+ }
54
+
55
+ if (schema && typeof schema === 'object') {
56
+ if (schema.body && typeof schema.body.parse === 'function') {
57
+ bodyType = zodToTsType(schema.body, 3);
58
+ }
59
+ if (schema.query && typeof schema.query.parse === 'function') {
60
+ queryType = zodToTsType(schema.query, 3);
61
+ }
62
+ if (schema.params && typeof schema.params.parse === 'function') {
63
+ paramsType = zodToTsType(schema.params, 3);
64
+ }
65
+ }
66
+
67
+ // Response type from def.response or doc
68
+ if (def && def.response) {
69
+ const resp200 = def.response[200] || def.response['200'] || def.response[201] || def.response['201'];
70
+ if (resp200 && typeof resp200.parse === 'function') {
71
+ responseType = zodToTsType(resp200, 3);
72
+ }
73
+ }
74
+ } catch (_) {}
75
+
76
+ return { bodyType, queryType, paramsType, responseType };
77
+ }
78
+
79
+ /**
80
+ * Collects and processes all API routes for client generation
81
+ * @param {string} rootDir
82
+ * @param {string} [pagesRelDir='pages']
83
+ * @returns {Array<Object>}
84
+ */
85
+ function collectApiRoutes(rootDir, pagesRelDir = 'pages') {
86
+ const pagesDir = path.resolve(rootDir, pagesRelDir);
87
+ const apiDir = path.join(pagesDir, 'api');
88
+ if (!fs.existsSync(apiDir)) return [];
89
+
90
+ let descriptors = [];
91
+ try {
92
+ descriptors = scanRoutes({
93
+ rootDir,
94
+ pages: { dir: pagesRelDir },
95
+ api: { dir: path.join(pagesRelDir, 'api') },
96
+ });
97
+ } catch {
98
+ return [];
99
+ }
100
+
101
+ const apiRoutes = descriptors.filter((d) => d.type === 'api' || d.file.includes(`${path.sep}api${path.sep}`));
102
+ const routes = [];
103
+
104
+ for (const desc of apiRoutes) {
105
+ const method = (desc.method || 'get').toLowerCase();
106
+ const pattern = desc.pattern || `/api/${path.basename(desc.file, '.js')}`;
107
+ const segments = pathToSegments(pattern);
108
+ const types = extractRouteTypes(desc.file);
109
+
110
+ routes.push({
111
+ pattern,
112
+ method,
113
+ segments,
114
+ file: desc.file,
115
+ ...types,
116
+ });
117
+ }
118
+
119
+ return routes;
120
+ }
121
+
122
+ /**
123
+ * Builds TypeScript client source code
124
+ * @param {Array<Object>} routes
125
+ * @param {Object} [options]
126
+ * @returns {string}
127
+ */
128
+ function generateClientCode(routes = [], options = {}) {
129
+ const timestamp = new Date().toISOString();
130
+ const defaultBaseUrl = options.baseUrl || '';
131
+
132
+ let code = `/**
133
+ * Webspresso Auto-Generated Type-Safe Frontend Client
134
+ * Generated on: ${timestamp}
135
+ * DO NOT EDIT DIRECTLY. Regenerate using: npx webspresso client:generate
136
+ */
137
+
138
+ export interface WebspressoClientOptions {
139
+ baseUrl?: string;
140
+ fetch?: typeof fetch;
141
+ headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
142
+ getToken?: () => string | null | Promise<string | null>;
143
+ }
144
+
145
+ export class WebspressoClientError<TData = any> extends Error {
146
+ constructor(
147
+ message: string,
148
+ public status: number,
149
+ public data: TData
150
+ ) {
151
+ super(message);
152
+ this.name = 'WebspressoClientError';
153
+ }
154
+ }
155
+
156
+ export interface ApiRequestOptions<TBody = any, TQuery = any, TParams = any> {
157
+ body?: TBody;
158
+ query?: TQuery;
159
+ params?: TParams;
160
+ headers?: Record<string, string>;
161
+ signal?: AbortSignal;
162
+ }
163
+
164
+ /* ============================================================================
165
+ * Endpoint Contract Definitions
166
+ * ========================================================================= */
167
+
168
+ `;
169
+
170
+ // Write contract interfaces for all scanned routes
171
+ routes.forEach((r, idx) => {
172
+ const routeId = `Route_${idx}`;
173
+ r.routeId = routeId;
174
+ code += `export interface ${routeId}_Req {\n`;
175
+ code += ` body: ${r.bodyType};\n`;
176
+ code += ` query: ${r.queryType};\n`;
177
+ code += ` params: ${r.paramsType};\n`;
178
+ code += `}\n`;
179
+ code += `export type ${routeId}_Res = ${r.responseType};\n\n`;
180
+ });
181
+
182
+ // Client proxy factory and types
183
+ code += `/* ============================================================================
184
+ * Type-Safe Client Interface & Factory
185
+ * ========================================================================= */
186
+
187
+ export interface WebspressoApiClient {
188
+ [key: string]: any;
189
+ }
190
+
191
+ /**
192
+ * Create a strongly-typed Webspresso API Client
193
+ * @param options Client configuration options
194
+ */
195
+ export function createWebspressoClient(options: WebspressoClientOptions = {}): WebspressoApiClient {
196
+ const baseUrl = (options.baseUrl || '${defaultBaseUrl}').replace(/\\/+$/, '');
197
+ const customFetch = options.fetch || (typeof fetch !== 'undefined' ? fetch : null);
198
+
199
+ if (!customFetch) {
200
+ throw new Error('[WebspressoClient] No global fetch found. Please provide a custom fetch in options.');
201
+ }
202
+
203
+ function createCaller(pathSegments: string[]): any {
204
+ return new Proxy(function () {}, {
205
+ get(_, prop: string) {
206
+ if (['get', 'post', 'put', 'patch', 'delete'].includes(prop)) {
207
+ return async (reqOpts: any = {}) => {
208
+ let urlPath = '/' + pathSegments.join('/');
209
+
210
+ if (reqOpts.params) {
211
+ for (const [k, v] of Object.entries(reqOpts.params)) {
212
+ urlPath = urlPath.replace(':' + k, encodeURIComponent(String(v)));
213
+ urlPath = urlPath.replace('[' + k + ']', encodeURIComponent(String(v)));
214
+ }
215
+ }
216
+
217
+ if (reqOpts.query) {
218
+ const searchParams = new URLSearchParams();
219
+ for (const [k, v] of Object.entries(reqOpts.query)) {
220
+ if (v !== undefined && v !== null) {
221
+ searchParams.append(k, String(v));
222
+ }
223
+ }
224
+ const qs = searchParams.toString();
225
+ if (qs) {
226
+ urlPath += (urlPath.includes('?') ? '&' : '?') + qs;
227
+ }
228
+ }
229
+
230
+ const fullUrl = baseUrl ? baseUrl + urlPath : urlPath;
231
+ const headers: Record<string, string> = {
232
+ Accept: 'application/json',
233
+ };
234
+
235
+ if (typeof options.headers === 'function') {
236
+ Object.assign(headers, await options.headers());
237
+ } else if (options.headers) {
238
+ Object.assign(headers, options.headers);
239
+ }
240
+
241
+ if (options.getToken) {
242
+ const token = await options.getToken();
243
+ if (token) {
244
+ headers['Authorization'] = 'Bearer ' + token;
245
+ }
246
+ }
247
+
248
+ if (reqOpts.headers) {
249
+ Object.assign(headers, reqOpts.headers);
250
+ }
251
+
252
+ const fetchOpts: RequestInit = {
253
+ method: prop.toUpperCase(),
254
+ headers,
255
+ signal: reqOpts.signal,
256
+ };
257
+
258
+ if (reqOpts.body !== undefined && prop !== 'get') {
259
+ headers['Content-Type'] = 'application/json';
260
+ fetchOpts.body = JSON.stringify(reqOpts.body);
261
+ }
262
+
263
+ const response = await customFetch(fullUrl, fetchOpts);
264
+ const contentType = response.headers.get('content-type') || '';
265
+ const isJson = contentType.includes('application/json');
266
+ const data = isJson ? await response.json() : await response.text();
267
+
268
+ if (!response.ok) {
269
+ const errMsg = (data && typeof data === 'object' && (data.message || data.error))
270
+ || response.statusText
271
+ || 'Request failed';
272
+ throw new WebspressoClientError(errMsg, response.status, data);
273
+ }
274
+
275
+ return data;
276
+ };
277
+ }
278
+
279
+ return createCaller([...pathSegments, prop]);
280
+ },
281
+ apply(_, __, args) {
282
+ if (args.length > 0) {
283
+ return createCaller([...pathSegments, String(args[0])]);
284
+ }
285
+ return createCaller(pathSegments);
286
+ },
287
+ });
288
+ }
289
+
290
+ return createCaller([]);
291
+ }
292
+
293
+ export default createWebspressoClient;
294
+ `;
295
+
296
+ return code;
297
+ }
298
+
299
+ /**
300
+ * Generates the frontend client file
301
+ * @param {Object} options
302
+ * @returns {{ outFile: string, routesCount: number }}
303
+ */
304
+ function generateClient(options = {}) {
305
+ const rootDir = path.resolve(options.rootDir || process.cwd());
306
+ const pagesDir = options.pagesDir || 'pages';
307
+ const defaultOut = fs.existsSync(path.join(rootDir, 'src'))
308
+ ? path.join(rootDir, 'src', 'client.ts')
309
+ : path.join(rootDir, '.webspresso', 'client.ts');
310
+ const outFile = path.resolve(rootDir, options.outFile || defaultOut);
311
+
312
+ const routes = collectApiRoutes(rootDir, pagesDir);
313
+ const content = generateClientCode(routes, options);
314
+
315
+ const outDir = path.dirname(outFile);
316
+ if (!fs.existsSync(outDir)) {
317
+ fs.mkdirSync(outDir, { recursive: true });
318
+ }
319
+
320
+ fs.writeFileSync(outFile, content, 'utf8');
321
+
322
+ return {
323
+ outFile,
324
+ routesCount: routes.length,
325
+ };
326
+ }
327
+
328
+ class WebspressoClientError extends Error {
329
+ constructor(message, status, data) {
330
+ super(message);
331
+ this.name = 'WebspressoClientError';
332
+ this.status = status;
333
+ this.data = data;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Creates a universal Proxy-based Webspresso API Client
339
+ * @param {Object} [options]
340
+ * @param {string} [options.baseUrl='']
341
+ * @param {Function} [options.fetch]
342
+ * @param {Object|Function} [options.headers]
343
+ * @param {Function} [options.getToken]
344
+ * @returns {any}
345
+ */
346
+ function createWebspressoClient(options = {}) {
347
+ const baseUrl = (options.baseUrl || '').replace(/\/+$/, '');
348
+ const customFetch = options.fetch !== undefined ? options.fetch : (typeof fetch !== 'undefined' ? fetch : null);
349
+
350
+ if (!customFetch) {
351
+ throw new Error('[WebspressoClient] No global fetch found. Please provide a custom fetch in options.');
352
+ }
353
+
354
+ function createCaller(pathSegments) {
355
+ return new Proxy(function () {}, {
356
+ get(_, prop) {
357
+ if (['get', 'post', 'put', 'patch', 'delete'].includes(prop)) {
358
+ return async (reqOpts = {}) => {
359
+ let urlPath = '/' + pathSegments.join('/');
360
+
361
+ if (reqOpts.params) {
362
+ for (const [k, v] of Object.entries(reqOpts.params)) {
363
+ urlPath = urlPath.replace(':' + k, encodeURIComponent(String(v)));
364
+ urlPath = urlPath.replace('[' + k + ']', encodeURIComponent(String(v)));
365
+ }
366
+ }
367
+
368
+ if (reqOpts.query) {
369
+ const searchParams = new URLSearchParams();
370
+ for (const [k, v] of Object.entries(reqOpts.query)) {
371
+ if (v !== undefined && v !== null) {
372
+ if (Array.isArray(v)) {
373
+ v.forEach((val) => searchParams.append(k, String(val)));
374
+ } else {
375
+ searchParams.append(k, String(v));
376
+ }
377
+ }
378
+ }
379
+ const qs = searchParams.toString();
380
+ if (qs) {
381
+ urlPath += (urlPath.includes('?') ? '&' : '?') + qs;
382
+ }
383
+ }
384
+
385
+ const fullUrl = baseUrl ? baseUrl + urlPath : urlPath;
386
+ const headers = {
387
+ Accept: 'application/json',
388
+ };
389
+
390
+ if (typeof options.headers === 'function') {
391
+ Object.assign(headers, await options.headers());
392
+ } else if (options.headers) {
393
+ Object.assign(headers, options.headers);
394
+ }
395
+
396
+ if (options.getToken) {
397
+ const token = await options.getToken();
398
+ if (token) {
399
+ headers['Authorization'] = 'Bearer ' + token;
400
+ }
401
+ }
402
+
403
+ if (reqOpts.headers) {
404
+ Object.assign(headers, reqOpts.headers);
405
+ }
406
+
407
+ const fetchOpts = {
408
+ method: prop.toUpperCase(),
409
+ headers,
410
+ signal: reqOpts.signal,
411
+ };
412
+
413
+ if (reqOpts.body !== undefined && prop !== 'get') {
414
+ headers['Content-Type'] = 'application/json';
415
+ fetchOpts.body = typeof reqOpts.body === 'string' ? reqOpts.body : JSON.stringify(reqOpts.body);
416
+ }
417
+
418
+ const response = await customFetch(fullUrl, fetchOpts);
419
+ const contentType = response.headers && typeof response.headers.get === 'function'
420
+ ? (response.headers.get('content-type') || '')
421
+ : '';
422
+ const isJson = contentType.includes('application/json');
423
+ const data = isJson ? await response.json() : await response.text();
424
+
425
+ if (!response.ok) {
426
+ const errMsg = (data && typeof data === 'object' && (data.message || data.error))
427
+ || response.statusText
428
+ || 'Request failed';
429
+ throw new WebspressoClientError(errMsg, response.status, data);
430
+ }
431
+
432
+ return data;
433
+ };
434
+ }
435
+
436
+ return createCaller([...pathSegments, prop]);
437
+ },
438
+ apply(_, __, args) {
439
+ if (args.length > 0) {
440
+ return createCaller([...pathSegments, String(args[0])]);
441
+ }
442
+ return createCaller(pathSegments);
443
+ },
444
+ });
445
+ }
446
+
447
+ return createCaller([]);
448
+ }
449
+
450
+ module.exports = {
451
+ generateClient,
452
+ generateClientCode,
453
+ collectApiRoutes,
454
+ pathToSegments,
455
+ extractRouteTypes,
456
+ createWebspressoClient,
457
+ WebspressoClientError,
458
+ };
@@ -96,15 +96,18 @@ function scanDirSafely(dirPath, baseDir = dirPath, checkWarnings = false) {
96
96
 
97
97
  let isWebspressoPackageCache = null;
98
98
  function checkIsWebspressoPackage() {
99
+ if (process.env.__WEBSPRESSO_TESTING__ === 'true') return true;
99
100
  if (isWebspressoPackageCache === null) {
100
101
  try {
101
102
  const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
102
- isWebspressoPackageCache = Boolean(pkg && pkg.name === 'webspresso');
103
+ if (pkg && pkg.name === 'webspresso') {
104
+ isWebspressoPackageCache = true;
105
+ }
103
106
  } catch (_) {
104
107
  isWebspressoPackageCache = false;
105
108
  }
106
109
  }
107
- return isWebspressoPackageCache;
110
+ return isWebspressoPackageCache === true;
108
111
  }
109
112
 
110
113
  /**
@@ -32,6 +32,7 @@ const {
32
32
  resolveNamedMiddleware,
33
33
  resolveMiddlewares,
34
34
  } = require('./routing/middleware-resolver');
35
+ const { escapeRegExp } = require('../core/utils/regex');
35
36
 
36
37
  const NOOP = () => {};
37
38
  const EMPTY_OBJECT = Object.freeze({});
@@ -372,9 +373,6 @@ function rewriteDynamicRouteMarkers(route) {
372
373
  return out;
373
374
  }
374
375
 
375
- function escapeRegExp(s) {
376
- return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
377
- }
378
376
 
379
377
  /**
380
378
  * Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'.
@@ -1485,6 +1483,8 @@ function mountPages(app, options) {
1485
1483
  service: req.service,
1486
1484
  db,
1487
1485
  sse: res.sse,
1486
+ streamText: res.streamText,
1487
+ streamJson: res.streamJson,
1488
1488
  });
1489
1489
 
1490
1490
  await fn(req, res, apiCtx);