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.
- package/.agents/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- 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
|
-
|
|
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
|
/**
|
package/src/file-router.js
CHANGED
|
@@ -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);
|