webspresso 0.0.112 → 0.0.113

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.
@@ -163,21 +163,39 @@ function extractUrlParamNames(routePath) {
163
163
  * @param {import('zod').ZodTypeAny | null} zSchema
164
164
  * @returns {{ acceptsFormData: boolean, fields: Record<string, { type: string, required: boolean, isFile: boolean, isArray: boolean }> | null, isStrictObject: boolean }}
165
165
  */
166
+ const {
167
+ getZodDef,
168
+ getZodTypeName,
169
+ getZodInner,
170
+ getZodArrayElement,
171
+ getZodObjectShape,
172
+ } = require('../../core/validation/zod-introspect');
173
+
174
+ function unwrapZodObject(zSchema) {
175
+ let current = zSchema;
176
+ while (current && getZodDef(current)) {
177
+ const tName = getZodTypeName(current);
178
+ if (tName === 'ZodEffects' || tName === 'ZodOptional' || tName === 'ZodDefault' || tName === 'ZodPipeline') {
179
+ current = getZodInner(current);
180
+ } else {
181
+ break;
182
+ }
183
+ }
184
+ return current;
185
+ }
186
+
166
187
  function inspectBodySchemaFields(zSchema) {
167
- if (!zSchema || !zSchema._def) {
188
+ if (!zSchema || !getZodDef(zSchema)) {
168
189
  return { acceptsFormData: false, fields: null, isStrictObject: false };
169
190
  }
170
191
 
171
- let current = zSchema;
172
- while (current && current._def && (current._def.typeName === 'ZodEffects' || current._def.typeName === 'ZodOptional' || current._def.typeName === 'ZodDefault')) {
173
- current = current._def.schema || current._def.innerType;
174
- }
192
+ const current = unwrapZodObject(zSchema);
175
193
 
176
- if (!current || !current._def || current._def.typeName !== 'ZodObject') {
194
+ if (!current || getZodTypeName(current) !== 'ZodObject') {
177
195
  return { acceptsFormData: false, fields: null, isStrictObject: false };
178
196
  }
179
197
 
180
- const shape = typeof current.shape === 'function' ? current.shape() : current.shape;
198
+ const shape = getZodObjectShape(current);
181
199
  if (!shape || typeof shape !== 'object') {
182
200
  return { acceptsFormData: false, fields: {}, isStrictObject: true };
183
201
  }
@@ -190,18 +208,16 @@ function inspectBodySchemaFields(zSchema) {
190
208
  let required = true;
191
209
  let isArray = false;
192
210
 
193
- while (inner && inner._def) {
194
- const tName = inner._def.typeName;
211
+ while (inner && getZodDef(inner)) {
212
+ const tName = getZodTypeName(inner);
195
213
  if (tName === 'ZodOptional' || tName === 'ZodDefault') {
196
214
  required = false;
197
- inner = inner._def.innerType;
198
- } else if (tName === 'ZodNullable') {
199
- inner = inner._def.innerType;
200
- } else if (tName === 'ZodEffects') {
201
- inner = inner._def.schema;
215
+ inner = getZodInner(inner);
216
+ } else if (tName === 'ZodNullable' || tName === 'ZodEffects' || tName === 'ZodPipeline') {
217
+ inner = getZodInner(inner);
202
218
  } else if (tName === 'ZodArray') {
203
219
  isArray = true;
204
- inner = inner._def.type;
220
+ inner = getZodArrayElement(inner);
205
221
  } else {
206
222
  break;
207
223
  }
@@ -213,7 +229,7 @@ function inspectBodySchemaFields(zSchema) {
213
229
  );
214
230
  if (isFile) acceptsFormData = true;
215
231
 
216
- const baseTypeName = inner && inner._def ? inner._def.typeName : 'ZodUnknown';
232
+ const baseTypeName = getZodTypeName(inner) || 'ZodUnknown';
217
233
  let scalarKind = 'string';
218
234
  if (isFile) scalarKind = 'file';
219
235
  else if (baseTypeName === 'ZodNumber') scalarKind = 'number';
@@ -271,12 +287,9 @@ function inferHandlerWireResponse(handlerFn, fileSource, schemas, zodToTsTypeFn)
271
287
  // 2. Build local variable type table from `req.input.body.<prop>` and literal assignments
272
288
  const bodyFieldTypes = {};
273
289
  if (schemas && schemas.bodySchema) {
274
- let current = schemas.bodySchema;
275
- while (current && current._def && (current._def.typeName === 'ZodEffects' || current._def.typeName === 'ZodOptional' || current._def.typeName === 'ZodDefault')) {
276
- current = current._def.schema || current._def.innerType;
277
- }
278
- if (current && current._def && current._def.typeName === 'ZodObject') {
279
- const shape = typeof current.shape === 'function' ? current.shape() : current.shape;
290
+ const current = unwrapZodObject(schemas.bodySchema);
291
+ if (current && getZodTypeName(current) === 'ZodObject') {
292
+ const shape = getZodObjectShape(current);
280
293
  if (shape) {
281
294
  for (const [k, v] of Object.entries(shape)) {
282
295
  bodyFieldTypes[k] = zodToTsTypeFn(v, 2, 'wire');
@@ -1141,6 +1154,50 @@ function checkTemplateFormContracts(rootDir, options = {}) {
1141
1154
  continue;
1142
1155
  }
1143
1156
 
1157
+ // Validate new client-behavior data-w-* attributes (skipping dynamic {{ ... }} / {% ... %} values)
1158
+ const ignoreAttrRe = /\bdata-w-ignore\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
1159
+ let attrMatch;
1160
+ while ((attrMatch = ignoreAttrRe.exec(content)) !== null) {
1161
+ const val = (attrMatch[1] ?? attrMatch[2] ?? '').trim().toLowerCase();
1162
+ if (!val || val.includes('{{') || val.includes('{%')) continue;
1163
+ if (val !== 'children' && val !== 'self' && val !== 'all' && val !== 'both') {
1164
+ warnings.push({
1165
+ code: 'INVALID_DATA_W_IGNORE',
1166
+ file: relFile,
1167
+ value: val,
1168
+ message: `Template "${relFile}" uses unsupported data-w-ignore="${val}". Expected "children", "self", or "all".`,
1169
+ });
1170
+ }
1171
+ }
1172
+
1173
+ const delayAttrRe = /\bdata-w-loading-delay\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
1174
+ while ((attrMatch = delayAttrRe.exec(content)) !== null) {
1175
+ const val = (attrMatch[1] ?? attrMatch[2] ?? '').trim();
1176
+ if (!val || val.includes('{{') || val.includes('{%')) continue;
1177
+ if (!/^\d+(?:\.\d+)?(?:ms|s)?$/i.test(val)) {
1178
+ warnings.push({
1179
+ code: 'INVALID_LOADING_DELAY',
1180
+ file: relFile,
1181
+ value: val,
1182
+ message: `Template "${relFile}" uses invalid data-w-loading-delay="${val}". Expected a non-negative number or duration (e.g. "200", "200ms", "1s").`,
1183
+ });
1184
+ }
1185
+ }
1186
+
1187
+ const errorModeRe = /\bdata-w-error-mode\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
1188
+ while ((attrMatch = errorModeRe.exec(content)) !== null) {
1189
+ const val = (attrMatch[1] ?? attrMatch[2] ?? '').trim().toLowerCase();
1190
+ if (!val || val.includes('{{') || val.includes('{%')) continue;
1191
+ if (val !== 'touched' && val !== 'always') {
1192
+ warnings.push({
1193
+ code: 'INVALID_ERROR_MODE',
1194
+ file: relFile,
1195
+ value: val,
1196
+ message: `Template "${relFile}" uses unsupported data-w-error-mode="${val}". Expected "touched" or "always".`,
1197
+ });
1198
+ }
1199
+ }
1200
+
1144
1201
  let formMatch;
1145
1202
  while ((formMatch = formRegex.exec(content)) !== null) {
1146
1203
  const formAttrs = formMatch[1] || '';
@@ -13,6 +13,16 @@ const { hasServiceRegistry, getServiceRegistry } = require('../../core/context')
13
13
  const { scanRoutes } = require('../../src/discovery/scan-routes');
14
14
  const { compileRouteTable } = require('../../src/routing/route-table');
15
15
  const { z } = require('../../core/validation');
16
+ const {
17
+ getZodDef,
18
+ getZodTypeName,
19
+ getZodInner,
20
+ getZodArrayElement,
21
+ getZodObjectShape,
22
+ getZodEnumValues,
23
+ getZodLiteralValue,
24
+ isZodCoerced,
25
+ } = require('../../core/validation/zod-introspect');
16
26
  const {
17
27
  collectApiRouteContracts: collectApiRouteContractsRaw,
18
28
  generateApiDeclarationCode,
@@ -49,7 +59,7 @@ function inferZodTransformOutputType(transformFn, innerSchema, indent, mode) {
49
59
  return mode === 'wire' ? 'string' : 'Date';
50
60
  }
51
61
  // Try executing transform on a safe dummy sample if innerSchema is primitive
52
- const innerType = innerSchema && innerSchema._def && innerSchema._def.typeName;
62
+ const innerType = getZodTypeName(innerSchema);
53
63
  try {
54
64
  let sample;
55
65
  if (innerType === 'ZodString') sample = '1';
@@ -79,14 +89,14 @@ function inferZodTransformOutputType(transformFn, innerSchema, indent, mode) {
79
89
  * @returns {boolean}
80
90
  */
81
91
  function isZodFieldOptional(zField, mode = 'output') {
82
- if (!zField || !zField._def) return false;
83
- const t = zField._def.typeName;
92
+ const t = getZodTypeName(zField);
93
+ if (!t) return false;
84
94
  if (t === 'ZodOptional') return true;
85
95
  if (t === 'ZodDefault') {
86
96
  return mode === 'input';
87
97
  }
88
- if (t === 'ZodEffects' && zField._def.schema) {
89
- return isZodFieldOptional(zField._def.schema, mode);
98
+ if ((t === 'ZodEffects' || t === 'ZodPipeline') && getZodInner(zField)) {
99
+ return isZodFieldOptional(getZodInner(zField), mode);
90
100
  }
91
101
  return false;
92
102
  }
@@ -95,13 +105,13 @@ function isZodFieldOptional(zField, mode = 'output') {
95
105
  * Unwraps outer `ZodOptional` / `ZodDefault` for property type emission inside `ZodObject`.
96
106
  */
97
107
  function unwrapOptionalOrDefault(zField, mode = 'output') {
98
- if (!zField || !zField._def) return zField;
99
- const t = zField._def.typeName;
108
+ const t = getZodTypeName(zField);
109
+ if (!t) return zField;
100
110
  if (t === 'ZodOptional') {
101
- return zField._def.innerType;
111
+ return getZodInner(zField);
102
112
  }
103
113
  if (t === 'ZodDefault' && mode === 'input') {
104
- return zField._def.innerType;
114
+ return getZodInner(zField);
105
115
  }
106
116
  return zField;
107
117
  }
@@ -114,8 +124,9 @@ function unwrapOptionalOrDefault(zField, mode = 'output') {
114
124
  * @returns {string}
115
125
  */
116
126
  function zodToTsType(zType, indent = 0, mode = 'output') {
117
- if (!zType || !zType._def) return 'unknown';
118
- const typeName = zType._def.typeName;
127
+ const def = getZodDef(zType);
128
+ const typeName = getZodTypeName(zType);
129
+ if (!def || !typeName) return 'unknown';
119
130
  const spaces = ' '.repeat(indent);
120
131
  const nextSpaces = ' '.repeat(indent + 1);
121
132
 
@@ -128,12 +139,12 @@ function zodToTsType(zType, indent = 0, mode = 'output') {
128
139
  case 'ZodString':
129
140
  return 'string';
130
141
  case 'ZodNumber':
131
- if (mode === 'input' && zType._def.coerce) {
142
+ if (mode === 'input' && isZodCoerced(zType)) {
132
143
  return 'number | string';
133
144
  }
134
145
  return 'number';
135
146
  case 'ZodBoolean':
136
- if (mode === 'input' && zType._def.coerce) {
147
+ if (mode === 'input' && isZodCoerced(zType)) {
137
148
  return 'boolean | string';
138
149
  }
139
150
  return 'boolean';
@@ -155,48 +166,63 @@ function zodToTsType(zType, indent = 0, mode = 'output') {
155
166
  return 'unknown';
156
167
  case 'ZodNever':
157
168
  return 'never';
158
- case 'ZodLiteral':
159
- return typeof zType._def.value === 'string'
160
- ? `'${zType._def.value}'`
161
- : String(zType._def.value);
169
+ case 'ZodLiteral': {
170
+ const litVal = getZodLiteralValue(zType);
171
+ return typeof litVal === 'string' ? `'${litVal}'` : String(litVal);
172
+ }
162
173
  case 'ZodEnum':
163
- return (zType._def.values || []).map((v) => `'${v}'`).join(' | ') || 'string';
174
+ return getZodEnumValues(zType).map((v) => `'${v}'`).join(' | ') || 'string';
164
175
  case 'ZodNativeEnum':
165
176
  return 'number | string';
166
177
  case 'ZodArray':
167
- return `Array<${zodToTsType(zType._def.type, indent, mode)}>`;
178
+ return `Array<${zodToTsType(getZodArrayElement(zType), indent, mode)}>`;
168
179
  case 'ZodOptional':
169
- return `${zodToTsType(zType._def.innerType, indent, mode)} | undefined`;
180
+ return `${zodToTsType(getZodInner(zType), indent, mode)} | undefined`;
170
181
  case 'ZodNullable':
171
- return `${zodToTsType(zType._def.innerType, indent, mode)} | null`;
182
+ return `${zodToTsType(getZodInner(zType), indent, mode)} | null`;
172
183
  case 'ZodDefault':
173
- return zodToTsType(zType._def.innerType, indent, mode);
184
+ return zodToTsType(getZodInner(zType), indent, mode);
185
+ case 'ZodPipeline': {
186
+ const inDef = getZodDef(def.in);
187
+ const outDef = getZodDef(def.out);
188
+ if (mode === 'input') {
189
+ if (inDef && inDef.type === 'transform' && def.out) {
190
+ return `${zodToTsType(def.out, indent, mode)} | string`;
191
+ }
192
+ return zodToTsType(def.in, indent, mode);
193
+ }
194
+ if (outDef && outDef.type === 'transform' && typeof outDef.transform === 'function') {
195
+ return inferZodTransformOutputType(outDef.transform, def.in, indent, mode);
196
+ }
197
+ return zodToTsType(def.out || def.in, indent, mode);
198
+ }
174
199
  case 'ZodEffects': {
175
200
  if (mode === 'input') {
176
- if (zType._def.effect && zType._def.effect.type === 'preprocess') {
177
- return `${zodToTsType(zType._def.schema, indent, mode)} | string`;
201
+ if (def.effect && def.effect.type === 'preprocess') {
202
+ return `${zodToTsType(def.schema, indent, mode)} | string`;
178
203
  }
179
- return zodToTsType(zType._def.schema, indent, mode);
204
+ return zodToTsType(def.schema, indent, mode);
180
205
  }
181
- if (zType._def.effect && zType._def.effect.type === 'transform') {
206
+ if (def.effect && def.effect.type === 'transform') {
182
207
  return inferZodTransformOutputType(
183
- zType._def.effect.transform,
184
- zType._def.schema,
208
+ def.effect.transform,
209
+ def.schema,
185
210
  indent,
186
211
  mode
187
212
  );
188
213
  }
189
- return zodToTsType(zType._def.schema, indent, mode);
214
+ return zodToTsType(def.schema, indent, mode);
190
215
  }
191
216
  case 'ZodUnion':
192
217
  case 'ZodDiscriminatedUnion':
193
- return (zType._def.options || []).map((opt) => zodToTsType(opt, indent, mode)).join(' | ') || 'unknown';
218
+ return (def.options || []).map((opt) => zodToTsType(opt, indent, mode)).join(' | ') || 'unknown';
194
219
  case 'ZodObject': {
195
- const shape = typeof zType.shape === 'function' ? zType.shape() : zType.shape;
220
+ const shape = getZodObjectShape(zType);
196
221
  if (!shape || Object.keys(shape).length === 0) return 'Record<string, unknown>';
197
222
  const entries = [];
198
223
  for (const [k, v] of Object.entries(shape)) {
199
- if (mode === 'wire' && v && v._def && (v._def.typeName === 'ZodUndefined' || v._def.typeName === 'ZodVoid')) {
224
+ const vType = getZodTypeName(v);
225
+ if (mode === 'wire' && (vType === 'ZodUndefined' || vType === 'ZodVoid')) {
200
226
  continue;
201
227
  }
202
228
  const isOpt = isZodFieldOptional(v, mode);
@@ -209,7 +235,7 @@ function zodToTsType(zType, indent = 0, mode = 'output') {
209
235
  return `{\n${entries.join('\n')}\n${spaces}}`;
210
236
  }
211
237
  case 'ZodRecord':
212
- return `Record<string, ${zodToTsType(zType._def.valueType, indent, mode)}>`;
238
+ return `Record<string, ${zodToTsType(def.valueType, indent, mode)}>`;
213
239
  default:
214
240
  return 'unknown';
215
241
  }
@@ -247,8 +273,8 @@ function collectModelMetadata(modelsDir) {
247
273
  const columns = [];
248
274
  if (model.schema && model.schema.shape) {
249
275
  for (const [colName, colZod] of Object.entries(model.schema.shape)) {
250
- const isOpt = colZod?._def?.typeName === 'ZodOptional';
251
- const innerZod = isOpt ? colZod._def.innerType : colZod;
276
+ const isOpt = getZodTypeName(colZod) === 'ZodOptional';
277
+ const innerZod = isOpt ? getZodInner(colZod) : colZod;
252
278
  columns.push({
253
279
  name: colName,
254
280
  isOptional: isOpt,
@@ -1998,12 +1998,30 @@ function mountPages(app, options) {
1998
1998
  resolveMiddlewareList: (mwList) => resolveMiddlewares(mwList, middlewares),
1999
1999
  });
2000
2000
 
2001
- const { rateLimit } = require('../core/rate-limit');
2002
- const fragmentLimiter = rateLimit({
2003
- window: 60000,
2004
- limit: 300,
2005
- skip: () => process.env.NODE_ENV === 'test',
2006
- });
2001
+ let fragmentLimiter;
2002
+ try {
2003
+ const expressRateLimit = require('express-rate-limit');
2004
+ const rateLimitFn = expressRateLimit.rateLimit || expressRateLimit;
2005
+ if (typeof rateLimitFn === 'function') {
2006
+ fragmentLimiter = rateLimitFn({
2007
+ windowMs: 60000,
2008
+ limit: 300,
2009
+ standardHeaders: 'draft-7',
2010
+ legacyHeaders: false,
2011
+ skip: () => process.env.NODE_ENV === 'test',
2012
+ });
2013
+ }
2014
+ } catch (_e) {
2015
+ // Fall back to built-in core/rate-limit when express-rate-limit peer is not installed
2016
+ }
2017
+ if (!fragmentLimiter) {
2018
+ const { rateLimit } = require('../core/rate-limit');
2019
+ fragmentLimiter = rateLimit({
2020
+ window: 60000,
2021
+ limit: 300,
2022
+ skip: () => process.env.NODE_ENV === 'test',
2023
+ });
2024
+ }
2007
2025
 
2008
2026
  if (typeof app.get === 'function') {
2009
2027
  app.get('/__webspresso/fragments', fragmentLimiter, fragmentRefreshHandler);
@@ -120,12 +120,30 @@ function mountDiscoveredRoutes(app, routeTable, context) {
120
120
  resolveMiddlewareList: (mwList) => resolveMiddlewares(mwList, context.middlewares || {}),
121
121
  });
122
122
 
123
- const { rateLimit } = require('../../core/rate-limit');
124
- const fragmentLimiter = rateLimit({
125
- window: 60000,
126
- limit: 300,
127
- skip: () => process.env.NODE_ENV === 'test',
128
- });
123
+ let fragmentLimiter;
124
+ try {
125
+ const expressRateLimit = require('express-rate-limit');
126
+ const rateLimitFn = expressRateLimit.rateLimit || expressRateLimit;
127
+ if (typeof rateLimitFn === 'function') {
128
+ fragmentLimiter = rateLimitFn({
129
+ windowMs: 60000,
130
+ limit: 300,
131
+ standardHeaders: 'draft-7',
132
+ legacyHeaders: false,
133
+ skip: () => process.env.NODE_ENV === 'test',
134
+ });
135
+ }
136
+ } catch (_e) {
137
+ // Fall back to built-in core/rate-limit when express-rate-limit peer is not installed
138
+ }
139
+ if (!fragmentLimiter) {
140
+ const { rateLimit } = require('../../core/rate-limit');
141
+ fragmentLimiter = rateLimit({
142
+ window: 60000,
143
+ limit: 300,
144
+ skip: () => process.env.NODE_ENV === 'test',
145
+ });
146
+ }
129
147
 
130
148
  app.get('/__webspresso/fragments', fragmentLimiter, fragmentRefreshHandler);
131
149
  app.post('/__webspresso/fragments', fragmentLimiter, fragmentRefreshHandler);