webspresso 0.0.112 → 0.0.114

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 (42) hide show
  1. package/README.md +26 -0
  2. package/bin/commands/admin-password.js +1 -7
  3. package/bin/commands/api.js +1 -7
  4. package/bin/commands/new.js +1 -7
  5. package/bin/commands/page.js +1 -7
  6. package/bin/commands/seed.js +1 -7
  7. package/bin/commands/service.js +1 -7
  8. package/bin/utils/prompt.js +17 -0
  9. package/core/cache/tagged-lru.js +9 -3
  10. package/core/client/adapter/datastar.js +602 -26
  11. package/core/client/api.js +151 -7
  12. package/core/client/controller.js +172 -11
  13. package/core/client/devtool.js +59 -33
  14. package/core/client/forms.js +479 -29
  15. package/core/client/fragments.js +456 -27
  16. package/core/client/index.js +18 -1
  17. package/core/client/server-fragments.js +125 -40
  18. package/core/client/utils.js +72 -17
  19. package/core/console/commands.js +26 -5
  20. package/core/console/repl.js +7 -1
  21. package/core/http/pipeline.js +82 -1
  22. package/core/openapi/build-from-api-routes.js +5 -5
  23. package/core/orm/json-fields.js +7 -6
  24. package/core/orm/schema-helpers.js +18 -15
  25. package/core/orm/utils/nanoid.js +7 -8
  26. package/core/plugins/plugin-manager.js +155 -1
  27. package/core/services/validator.js +12 -1
  28. package/core/validation/apply.js +3 -0
  29. package/core/validation/index.js +25 -0
  30. package/core/validation/zod-introspect.js +187 -0
  31. package/index.d.ts +113 -1
  32. package/package.json +12 -11
  33. package/plugins/mcp/providers/services.js +2 -2
  34. package/src/api/define-api.js +2 -0
  35. package/src/api/validation-i18n.js +6 -2
  36. package/src/bundle/index.js +140 -6
  37. package/src/dev/api-typegen.js +79 -22
  38. package/src/dev/typegen.js +61 -35
  39. package/src/dev-reload/index.js +44 -2
  40. package/src/file-router.js +24 -6
  41. package/src/routing/mount-discovered-routes.js +24 -6
  42. package/src/server.js +59 -3
@@ -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,
@@ -168,8 +168,39 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
168
168
  document.addEventListener('keydown', onKeyDown);
169
169
  }
170
170
 
171
+ function saveScrollPosition() {
172
+ try {
173
+ if (typeof window.sessionStorage !== 'undefined') {
174
+ window.sessionStorage.setItem('__ws_dev_scroll', JSON.stringify({
175
+ url: window.location.pathname + window.location.search,
176
+ x: window.scrollX || 0,
177
+ y: window.scrollY || 0,
178
+ }));
179
+ }
180
+ } catch (_) {}
181
+ }
182
+
183
+ function restoreScrollPosition() {
184
+ try {
185
+ if (typeof window.sessionStorage === 'undefined') return;
186
+ var raw = window.sessionStorage.getItem('__ws_dev_scroll');
187
+ if (!raw) return;
188
+ window.sessionStorage.removeItem('__ws_dev_scroll');
189
+ var pos = JSON.parse(raw);
190
+ if (pos && pos.url === (window.location.pathname + window.location.search)) {
191
+ var apply = function() { window.scrollTo(pos.x || 0, pos.y || 0); };
192
+ if (typeof window.requestAnimationFrame === 'function') {
193
+ window.requestAnimationFrame(apply);
194
+ } else {
195
+ apply();
196
+ }
197
+ }
198
+ } catch (_) {}
199
+ }
200
+
171
201
  function safeReload(reason) {
172
202
  clearOverlay();
203
+ saveScrollPosition();
173
204
  var reasonMsg = reason ? ' (' + reason + ')' : '';
174
205
  console.log('[Webspresso] 🔄 Reloading page' + reasonMsg + '...');
175
206
 
@@ -246,10 +277,15 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
246
277
  };
247
278
  }
248
279
 
280
+ function init() {
281
+ restoreScrollPosition();
282
+ connect();
283
+ }
284
+
249
285
  if (document.readyState === 'loading') {
250
- document.addEventListener('DOMContentLoaded', connect);
286
+ document.addEventListener('DOMContentLoaded', init);
251
287
  } else {
252
- connect();
288
+ init();
253
289
  }
254
290
  })();
255
291
  `;
@@ -290,6 +326,7 @@ function createDevReloadHub(config = {}) {
290
326
  const autoInject = options.autoInject !== false;
291
327
  const watchPublicAll = options.watchPublicAll === true;
292
328
  const extraWatch = Array.isArray(options.watch) ? options.watch : [];
329
+ let ignoreClientFiles = Boolean(options.ignoreClientFiles);
293
330
 
294
331
  const templateExtensions = new Set(
295
332
  (Array.isArray(options.templateExtensions)
@@ -393,6 +430,9 @@ function createDevReloadHub(config = {}) {
393
430
  setDevMetaProvider: (fn) => {
394
431
  getDevMetaCallback = fn;
395
432
  },
433
+ setIgnoreClientFiles: (flag) => {
434
+ ignoreClientFiles = Boolean(flag);
435
+ },
396
436
  invalidateTemplateCache,
397
437
  triggerTemplateReload: (filename, reason = 'template') => {
398
438
  onTemplateFileChanged(filename, reason);
@@ -594,6 +634,7 @@ function createDevReloadHub(config = {}) {
594
634
  if (watchTemplates && viewsDir && fs.existsSync(viewsDir)) {
595
635
  watchDirectory(viewsDir, (eventType, filename) => {
596
636
  if (!filename || shouldIgnore(filename)) return;
637
+ if (ignoreClientFiles && filename.includes('.client.')) return;
597
638
  const ext = path.extname(filename).toLowerCase();
598
639
  if (ext === '.css') {
599
640
  onCssFileChanged(filename);
@@ -607,6 +648,7 @@ function createDevReloadHub(config = {}) {
607
648
  if (watchTemplates && pagesDir && fs.existsSync(pagesDir)) {
608
649
  watchDirectory(pagesDir, (eventType, filename) => {
609
650
  if (!filename || shouldIgnore(filename)) return;
651
+ if (ignoreClientFiles && filename.includes('.client.')) return;
610
652
  const ext = path.extname(filename).toLowerCase();
611
653
  if (ext === '.css') {
612
654
  onCssFileChanged(filename);
@@ -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);
package/src/server.js CHANGED
@@ -24,7 +24,7 @@ const { resolveClientRuntime } = require('./client-runtime/resolve');
24
24
  const { mountPages, detectLocale, loadI18n, createTranslator } = require('./file-router');
25
25
  const { configureAssets, createHelpers, getScriptInjector, AssetManager } = require('./helpers');
26
26
  const { createDevReloadHub } = require('./dev-reload');
27
- const { createDevBundler } = require('./bundle');
27
+ const { createDevBundler, findDefaultEntryPoints } = require('./bundle');
28
28
  const { registerClearHook } = require('./njk-frontmatter');
29
29
  const { createPluginManager } = require('./plugin-manager');
30
30
  const { createServiceRegistry } = require('./services');
@@ -1558,15 +1558,19 @@ function createApp(options = {}) {
1558
1558
  // Client-side JS incremental bundler (esbuild context() API)
1559
1559
  const bundleOption = options.bundle;
1560
1560
  const isBundleExplicit = bundleOption === true || (typeof bundleOption === 'object' && bundleOption.enabled !== false);
1561
- const isBundleDisabled = bundleOption === false;
1561
+ const isBundleDisabled = bundleOption === false || (typeof bundleOption === 'object' && bundleOption.enabled === false);
1562
+ const shouldAutoBundle = bundleOption === undefined && isDev && !isTest && findDefaultEntryPoints(rootDir).length > 0;
1562
1563
 
1563
- if (isBundleExplicit && !isBundleDisabled) {
1564
+ if ((isBundleExplicit || shouldAutoBundle) && !isBundleDisabled) {
1564
1565
  const bundlerOpts = typeof bundleOption === 'object' ? { ...bundleOption } : {};
1565
1566
  if (!bundlerOpts.cwd) bundlerOpts.cwd = rootDir;
1566
1567
  const devBundler = createDevBundler(bundlerOpts, devReloadHub);
1567
1568
  app.devBundler = devBundler;
1568
1569
 
1569
1570
  if (devBundler.isAvailable() && bundlerOpts.watch !== false && isDev && !isTest) {
1571
+ if (devReloadHub && typeof devReloadHub.setIgnoreClientFiles === 'function') {
1572
+ devReloadHub.setIgnoreClientFiles(true);
1573
+ }
1570
1574
  devBundler.startWatch().catch((err) => {
1571
1575
  if (logging) console.error('[Webspresso Bundler] Watch error:', err.message);
1572
1576
  });
@@ -1577,6 +1581,58 @@ function createApp(options = {}) {
1577
1581
  }
1578
1582
  }
1579
1583
 
1584
+ // Auto-refresh generated TypeScript types & typed API client in dev mode when models/services/api routes change
1585
+ if (isDev && !isTest && options.typegen !== false) {
1586
+ const typegenWatchDirs = [
1587
+ path.join(rootDir, 'models'),
1588
+ path.join(rootDir, 'services'),
1589
+ path.join(rootDir, 'pages', 'api'),
1590
+ ].filter((d) => fs.existsSync(d));
1591
+
1592
+ if (typegenWatchDirs.length > 0) {
1593
+ const typegenWatchers = [];
1594
+ let typegenTimer = null;
1595
+ const scheduleTypegenRefresh = () => {
1596
+ if (typegenTimer) clearTimeout(typegenTimer);
1597
+ typegenTimer = setTimeout(() => {
1598
+ typegenTimer = null;
1599
+ try {
1600
+ const { generateTypes } = require('./dev/typegen');
1601
+ generateTypes({ rootDir, silent: true });
1602
+ const hasGeneratedClient =
1603
+ fs.existsSync(path.join(rootDir, '.webspresso', 'client.ts')) ||
1604
+ fs.existsSync(path.join(rootDir, 'src', 'client.ts'));
1605
+ if (hasGeneratedClient) {
1606
+ const { generateClient } = require('./dev/client-gen');
1607
+ generateClient({ rootDir, silent: true });
1608
+ }
1609
+ } catch (_) {}
1610
+ }, 250);
1611
+ if (typeof typegenTimer.unref === 'function') typegenTimer.unref();
1612
+ };
1613
+
1614
+ for (const watchDir of typegenWatchDirs) {
1615
+ try {
1616
+ const w = fs.watch(watchDir, { recursive: true }, (_evt, filename) => {
1617
+ if (!filename || filename.includes('node_modules') || filename.includes('.webspresso')) return;
1618
+ scheduleTypegenRefresh();
1619
+ });
1620
+ w.on('error', () => {});
1621
+ typegenWatchers.push(w);
1622
+ } catch (_) {}
1623
+ }
1624
+
1625
+ if (shutdownManager) {
1626
+ shutdownManager.registerDisposer('devTypegen', () => {
1627
+ if (typegenTimer) clearTimeout(typegenTimer);
1628
+ for (const w of typegenWatchers) {
1629
+ try { w.close(); } catch (_) {}
1630
+ }
1631
+ });
1632
+ }
1633
+ }
1634
+ }
1635
+
1580
1636
  // Configure Nunjucks with viewsDir priority and circular extension guard
1581
1637
  const templateDirs = [viewsDir, pagesDir].filter(Boolean);
1582
1638
  if (templateDirs.length === 0) {