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.
- package/README.md +26 -0
- package/bin/commands/admin-password.js +1 -7
- package/bin/commands/api.js +1 -7
- package/bin/commands/new.js +1 -7
- package/bin/commands/page.js +1 -7
- package/bin/commands/seed.js +1 -7
- package/bin/commands/service.js +1 -7
- package/bin/utils/prompt.js +17 -0
- package/core/cache/tagged-lru.js +9 -3
- package/core/client/adapter/datastar.js +602 -26
- package/core/client/api.js +151 -7
- package/core/client/controller.js +172 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +479 -29
- package/core/client/fragments.js +456 -27
- package/core/client/index.js +18 -1
- package/core/client/server-fragments.js +125 -40
- package/core/client/utils.js +72 -17
- package/core/console/commands.js +26 -5
- package/core/console/repl.js +7 -1
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/plugins/plugin-manager.js +155 -1
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +113 -1
- package/package.json +12 -11
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +140 -6
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/dev-reload/index.js +44 -2
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
- package/src/server.js +59 -3
package/src/dev/api-typegen.js
CHANGED
|
@@ -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
|
|
188
|
+
if (!zSchema || !getZodDef(zSchema)) {
|
|
168
189
|
return { acceptsFormData: false, fields: null, isStrictObject: false };
|
|
169
190
|
}
|
|
170
191
|
|
|
171
|
-
|
|
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 ||
|
|
194
|
+
if (!current || getZodTypeName(current) !== 'ZodObject') {
|
|
177
195
|
return { acceptsFormData: false, fields: null, isStrictObject: false };
|
|
178
196
|
}
|
|
179
197
|
|
|
180
|
-
const 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
|
|
194
|
-
const tName = inner
|
|
211
|
+
while (inner && getZodDef(inner)) {
|
|
212
|
+
const tName = getZodTypeName(inner);
|
|
195
213
|
if (tName === 'ZodOptional' || tName === 'ZodDefault') {
|
|
196
214
|
required = false;
|
|
197
|
-
inner = inner
|
|
198
|
-
} else if (tName === 'ZodNullable') {
|
|
199
|
-
inner = inner
|
|
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
|
|
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
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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] || '';
|
package/src/dev/typegen.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
89
|
-
return isZodFieldOptional(zField
|
|
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
|
-
|
|
99
|
-
|
|
108
|
+
const t = getZodTypeName(zField);
|
|
109
|
+
if (!t) return zField;
|
|
100
110
|
if (t === 'ZodOptional') {
|
|
101
|
-
return zField
|
|
111
|
+
return getZodInner(zField);
|
|
102
112
|
}
|
|
103
113
|
if (t === 'ZodDefault' && mode === 'input') {
|
|
104
|
-
return zField
|
|
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
|
-
|
|
118
|
-
const typeName = zType
|
|
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
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
case 'ZodLiteral': {
|
|
170
|
+
const litVal = getZodLiteralValue(zType);
|
|
171
|
+
return typeof litVal === 'string' ? `'${litVal}'` : String(litVal);
|
|
172
|
+
}
|
|
162
173
|
case 'ZodEnum':
|
|
163
|
-
return (zType
|
|
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
|
|
178
|
+
return `Array<${zodToTsType(getZodArrayElement(zType), indent, mode)}>`;
|
|
168
179
|
case 'ZodOptional':
|
|
169
|
-
return `${zodToTsType(zType
|
|
180
|
+
return `${zodToTsType(getZodInner(zType), indent, mode)} | undefined`;
|
|
170
181
|
case 'ZodNullable':
|
|
171
|
-
return `${zodToTsType(zType
|
|
182
|
+
return `${zodToTsType(getZodInner(zType), indent, mode)} | null`;
|
|
172
183
|
case 'ZodDefault':
|
|
173
|
-
return zodToTsType(zType
|
|
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 (
|
|
177
|
-
return `${zodToTsType(
|
|
201
|
+
if (def.effect && def.effect.type === 'preprocess') {
|
|
202
|
+
return `${zodToTsType(def.schema, indent, mode)} | string`;
|
|
178
203
|
}
|
|
179
|
-
return zodToTsType(
|
|
204
|
+
return zodToTsType(def.schema, indent, mode);
|
|
180
205
|
}
|
|
181
|
-
if (
|
|
206
|
+
if (def.effect && def.effect.type === 'transform') {
|
|
182
207
|
return inferZodTransformOutputType(
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
def.effect.transform,
|
|
209
|
+
def.schema,
|
|
185
210
|
indent,
|
|
186
211
|
mode
|
|
187
212
|
);
|
|
188
213
|
}
|
|
189
|
-
return zodToTsType(
|
|
214
|
+
return zodToTsType(def.schema, indent, mode);
|
|
190
215
|
}
|
|
191
216
|
case 'ZodUnion':
|
|
192
217
|
case 'ZodDiscriminatedUnion':
|
|
193
|
-
return (
|
|
218
|
+
return (def.options || []).map((opt) => zodToTsType(opt, indent, mode)).join(' | ') || 'unknown';
|
|
194
219
|
case 'ZodObject': {
|
|
195
|
-
const 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
|
-
|
|
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(
|
|
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
|
|
251
|
-
const innerZod = isOpt ? 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,
|
package/src/dev-reload/index.js
CHANGED
|
@@ -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',
|
|
286
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
251
287
|
} else {
|
|
252
|
-
|
|
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);
|
package/src/file-router.js
CHANGED
|
@@ -1998,12 +1998,30 @@ function mountPages(app, options) {
|
|
|
1998
1998
|
resolveMiddlewareList: (mwList) => resolveMiddlewares(mwList, middlewares),
|
|
1999
1999
|
});
|
|
2000
2000
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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) {
|