@lowdefy/build 5.5.1 → 6.0.0
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/dist/build/buildAgents.js +70 -17
- package/dist/build/buildApi/buildApi.js +4 -1
- package/dist/build/buildApi/buildEndpoint.js +8 -2
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
- package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
- package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +130 -0
- package/dist/build/buildAuth/buildAgentAuth.js +69 -0
- package/dist/build/buildAuth/buildAuth.js +14 -0
- package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
- package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
- package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
- package/dist/build/buildAuth/getAgentRoles.js +39 -0
- package/dist/build/buildAuth/getProtectedAgents.js +29 -0
- package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/rejectReservedEntityIds.js +52 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- package/dist/build/buildImports/buildImportsDev.js +15 -0
- package/dist/build/buildImports/buildImportsProd.js +4 -1
- package/dist/build/buildJs/generateClientJsModule.js +27 -0
- package/dist/build/buildJs/writeJs.js +3 -5
- package/dist/build/buildMcp.js +73 -0
- package/dist/build/buildModuleDefs.js +90 -48
- package/dist/build/buildModules.js +61 -14
- package/dist/build/buildNotifications.js +98 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
- package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
- package/dist/build/buildPages/buildBlock/buildEvents.js +197 -22
- package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
- package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
- package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
- package/dist/build/buildPages/buildBlock/validateBlock.js +19 -1
- package/dist/build/buildPages/buildPage.js +27 -3
- package/dist/build/buildPages/buildSubscriptions.js +145 -0
- package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
- package/dist/build/buildPages/validateStateReferences.js +4 -2
- package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
- package/dist/build/buildRefs/buildRefs.js +21 -12
- package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
- package/dist/build/buildRefs/deferredRegions.js +80 -0
- package/dist/build/buildRefs/deferredRegistry.js +292 -0
- package/dist/build/buildRefs/getKey.js +16 -5
- package/dist/build/buildRefs/getModuleRefContent.js +21 -6
- package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
- package/dist/build/buildRefs/parseRefContent.js +24 -0
- package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
- package/dist/build/buildRefs/walker.js +490 -209
- package/dist/build/buildTypes.js +22 -1
- package/dist/build/buildWebsockets.js +99 -0
- package/dist/build/collectPageContent.js +7 -1
- package/dist/build/copyAgentFileSystems.js +2 -2
- package/dist/build/fetchModules.js +18 -4
- package/dist/build/full/buildPages.js +16 -0
- package/dist/build/full/updateServerPackageJson.js +21 -0
- package/dist/build/jit/buildPageJit.js +46 -6
- package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
- package/dist/build/jit/pageContentKeys.js +1 -0
- package/dist/build/jit/shallowBuild.js +81 -1
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +149 -42
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +4 -0
- package/dist/build/validateCronConfig.js +110 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +58 -1
- package/dist/build/writeI18n.js +8 -1
- package/dist/build/writeMcp.js +21 -0
- package/dist/build/writeNotifications.js +30 -0
- package/dist/build/writePluginImports/importPluginModule.js +39 -0
- package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writeAuthImports.js +4 -0
- package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
- package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
- package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
- package/dist/build/writePluginImports/writeGlobalsCss.js +20 -11
- package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
- package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writePluginImports.js +18 -4
- package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
- package/dist/build/writeWebsockets.js +30 -0
- package/dist/createContext.js +6 -1
- package/dist/defaultPackages.js +7 -1
- package/dist/defaultTypesMap.js +689 -386
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +78 -8
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +861 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +20 -1
- package/dist/test-utils/runBuildForSnapshots.js +34 -4
- package/dist/test-utils/testContext.js +8 -2
- package/dist/utils/countOperators.js +5 -17
- package/dist/utils/createPluginTypesMap.js +21 -0
- package/dist/utils/getCronEnvironmentNames.js +25 -0
- package/dist/utils/logCollectedErrors.js +44 -10
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/validateId.js +11 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +66 -59
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { type } from '@lowdefy/helpers';
|
|
15
|
+
*/ import { isReserved, type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
17
|
import createCheckDuplicateId from '../../../utils/createCheckDuplicateId.js';
|
|
18
18
|
const BROWSER_DEFAULT_SHORTCUTS = new Set([
|
|
@@ -23,7 +23,43 @@ const BROWSER_DEFAULT_SHORTCUTS = new Set([
|
|
|
23
23
|
'mod+q',
|
|
24
24
|
'mod+l'
|
|
25
25
|
]);
|
|
26
|
-
|
|
26
|
+
const CONTROL_KEYS = [
|
|
27
|
+
':if',
|
|
28
|
+
':switch',
|
|
29
|
+
':return'
|
|
30
|
+
];
|
|
31
|
+
const ACTION_KEYS_NOT_ALLOWED_ON_CONTROLS = [
|
|
32
|
+
'id',
|
|
33
|
+
'skip',
|
|
34
|
+
'messages'
|
|
35
|
+
];
|
|
36
|
+
const CONTROL_ALLOWED_KEYS = {
|
|
37
|
+
':if': [
|
|
38
|
+
':if',
|
|
39
|
+
':then',
|
|
40
|
+
':else'
|
|
41
|
+
],
|
|
42
|
+
':return': [
|
|
43
|
+
':return'
|
|
44
|
+
],
|
|
45
|
+
':switch': [
|
|
46
|
+
':switch',
|
|
47
|
+
':default'
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
const CASE_ALLOWED_KEYS = [
|
|
51
|
+
':case',
|
|
52
|
+
':then'
|
|
53
|
+
];
|
|
54
|
+
// '~'-prefixed keys are build meta markers (~k, ~r, ~l, ~ignoreBuildChecks) and
|
|
55
|
+
// never user config - tolerate them by prefix, as the rest of the build does.
|
|
56
|
+
function isMetaKey(key) {
|
|
57
|
+
return key.startsWith('~');
|
|
58
|
+
}
|
|
59
|
+
function isControl(item) {
|
|
60
|
+
return type.isObject(item) && CONTROL_KEYS.some((key)=>key in item);
|
|
61
|
+
}
|
|
62
|
+
function checkAction(action, { blockId, callApiActionRefs, checkDuplicateActionId, eventId, linkActionRefs, pageId, requestActionRefs, typeCounters, websocketActionRefs }) {
|
|
27
63
|
const configKey = action['~k'];
|
|
28
64
|
if (type.isUndefined(action.id)) {
|
|
29
65
|
throw new ConfigError(`Action id missing on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
@@ -108,6 +144,142 @@ function checkAction(action, { blockId, callApiActionRefs, checkDuplicateActionI
|
|
|
108
144
|
});
|
|
109
145
|
}
|
|
110
146
|
}
|
|
147
|
+
// Collect static Subscribe/Unsubscribe/Publish action references for validation
|
|
148
|
+
if ([
|
|
149
|
+
'Subscribe',
|
|
150
|
+
'Unsubscribe'
|
|
151
|
+
].includes(action.type) && type.isString(action.params)) {
|
|
152
|
+
(websocketActionRefs ?? []).push({
|
|
153
|
+
websocketId: action.params,
|
|
154
|
+
action,
|
|
155
|
+
actionType: action.type,
|
|
156
|
+
blockId,
|
|
157
|
+
eventId,
|
|
158
|
+
sourcePageId: pageId
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (action.type === 'Publish' && type.isObject(action.params) && type.isString(action.params.websocketId)) {
|
|
162
|
+
(websocketActionRefs ?? []).push({
|
|
163
|
+
websocketId: action.params.websocketId,
|
|
164
|
+
action,
|
|
165
|
+
actionType: action.type,
|
|
166
|
+
blockId,
|
|
167
|
+
eventId,
|
|
168
|
+
sourcePageId: pageId
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function checkBranchList({ list, listName, configKey }, ctx) {
|
|
173
|
+
const { blockId, eventId, pageId } = ctx;
|
|
174
|
+
if (!type.isArray(list)) {
|
|
175
|
+
throw new ConfigError(`Control "${listName}" must be an array on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
176
|
+
received: list,
|
|
177
|
+
configKey
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
checkActionList(list, ctx);
|
|
181
|
+
}
|
|
182
|
+
function checkControl(control, ctx) {
|
|
183
|
+
const { blockId, eventId, pageId } = ctx;
|
|
184
|
+
const configKey = control['~k'];
|
|
185
|
+
const controlKeys = CONTROL_KEYS.filter((key)=>key in control);
|
|
186
|
+
if (controlKeys.length > 1) {
|
|
187
|
+
const keyList = controlKeys.map((key)=>`"${key}"`).join(', ');
|
|
188
|
+
throw new ConfigError(`Control has more than one control key (${keyList}) on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
189
|
+
received: controlKeys,
|
|
190
|
+
configKey
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const [controlKey] = controlKeys;
|
|
194
|
+
const invalidKeys = Object.keys(control).filter((key)=>!CONTROL_ALLOWED_KEYS[controlKey].includes(key) && !isMetaKey(key));
|
|
195
|
+
if (invalidKeys.length > 0) {
|
|
196
|
+
// Action keys do nothing on a control - call out the likely mistake directly.
|
|
197
|
+
if (ACTION_KEYS_NOT_ALLOWED_ON_CONTROLS.includes(invalidKeys[0])) {
|
|
198
|
+
throw new ConfigError(`Control "${controlKey}" can not have action property "${invalidKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
199
|
+
received: invalidKeys,
|
|
200
|
+
configKey
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw new ConfigError(`Control "${controlKey}" has invalid key "${invalidKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
204
|
+
received: invalidKeys,
|
|
205
|
+
configKey
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (controlKey === ':if') {
|
|
209
|
+
if (type.isNone(control[':then'])) {
|
|
210
|
+
throw new ConfigError(`Control ":if" requires a ":then" list on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
211
|
+
configKey
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
checkBranchList({
|
|
215
|
+
list: control[':then'],
|
|
216
|
+
listName: ':then',
|
|
217
|
+
configKey
|
|
218
|
+
}, ctx);
|
|
219
|
+
if (!type.isNone(control[':else'])) {
|
|
220
|
+
checkBranchList({
|
|
221
|
+
list: control[':else'],
|
|
222
|
+
listName: ':else',
|
|
223
|
+
configKey
|
|
224
|
+
}, ctx);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (controlKey === ':switch') {
|
|
228
|
+
if (!type.isArray(control[':switch'])) {
|
|
229
|
+
throw new ConfigError(`Control ":switch" must be an array of case objects on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
230
|
+
received: control[':switch'],
|
|
231
|
+
configKey
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
control[':switch'].forEach((caseObject)=>{
|
|
235
|
+
if (!type.isObject(caseObject)) {
|
|
236
|
+
throw new ConfigError(`Control ":switch" case must be an object on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
237
|
+
received: caseObject,
|
|
238
|
+
configKey
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const caseConfigKey = caseObject['~k'] ?? configKey;
|
|
242
|
+
if (!(':case' in caseObject)) {
|
|
243
|
+
throw new ConfigError(`Control ":switch" case requires a ":case" condition on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
244
|
+
configKey: caseConfigKey
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (type.isNone(caseObject[':then'])) {
|
|
248
|
+
throw new ConfigError(`Control ":case" requires a ":then" list on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
249
|
+
configKey: caseConfigKey
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const invalidCaseKeys = Object.keys(caseObject).filter((key)=>!CASE_ALLOWED_KEYS.includes(key) && !isMetaKey(key));
|
|
253
|
+
if (invalidCaseKeys.length > 0) {
|
|
254
|
+
throw new ConfigError(`Control ":switch" case has invalid key "${invalidCaseKeys[0]}" on event "${eventId}" on block "${blockId}" on page "${pageId}".`, {
|
|
255
|
+
received: invalidCaseKeys,
|
|
256
|
+
configKey: caseConfigKey
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
checkBranchList({
|
|
260
|
+
list: caseObject[':then'],
|
|
261
|
+
listName: ':then',
|
|
262
|
+
configKey: caseConfigKey
|
|
263
|
+
}, ctx);
|
|
264
|
+
});
|
|
265
|
+
if (!type.isNone(control[':default'])) {
|
|
266
|
+
checkBranchList({
|
|
267
|
+
list: control[':default'],
|
|
268
|
+
listName: ':default',
|
|
269
|
+
configKey
|
|
270
|
+
}, ctx);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// ':return' takes a value, not a list - nothing more to validate.
|
|
274
|
+
}
|
|
275
|
+
function checkActionList(list, ctx) {
|
|
276
|
+
list.forEach((item)=>{
|
|
277
|
+
if (isControl(item)) {
|
|
278
|
+
checkControl(item, ctx);
|
|
279
|
+
} else {
|
|
280
|
+
checkAction(item, ctx);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
111
283
|
}
|
|
112
284
|
function buildEvents(block, pageContext) {
|
|
113
285
|
if (block.events) {
|
|
@@ -143,26 +315,19 @@ function buildEvents(block, pageContext) {
|
|
|
143
315
|
const checkDuplicateActionId = createCheckDuplicateId({
|
|
144
316
|
message: 'Duplicate actionId "{{ id }}" on event "{{ eventId }}" on block "{{ blockId }}" on page "{{ pageId }}".'
|
|
145
317
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
callApiActionRefs: pageContext.callApiActionRefs,
|
|
160
|
-
typeCounters: pageContext.typeCounters,
|
|
161
|
-
pageId: pageContext.pageId,
|
|
162
|
-
linkActionRefs: pageContext.linkActionRefs,
|
|
163
|
-
requestActionRefs: pageContext.requestActionRefs,
|
|
164
|
-
checkDuplicateActionId
|
|
165
|
-
}));
|
|
318
|
+
const actionContext = {
|
|
319
|
+
eventId: key,
|
|
320
|
+
blockId: block.blockId,
|
|
321
|
+
callApiActionRefs: pageContext.callApiActionRefs,
|
|
322
|
+
typeCounters: pageContext.typeCounters,
|
|
323
|
+
pageId: pageContext.pageId,
|
|
324
|
+
linkActionRefs: pageContext.linkActionRefs,
|
|
325
|
+
requestActionRefs: pageContext.requestActionRefs,
|
|
326
|
+
websocketActionRefs: pageContext.websocketActionRefs,
|
|
327
|
+
checkDuplicateActionId
|
|
328
|
+
};
|
|
329
|
+
checkActionList(block.events[key].try, actionContext);
|
|
330
|
+
checkActionList(block.events[key].catch, actionContext);
|
|
166
331
|
// Validate shortcut strings and collect refs for duplicate detection
|
|
167
332
|
if (type.isObject(block.events[key]) && !type.isNone(block.events[key].shortcut)) {
|
|
168
333
|
const shortcuts = type.isArray(block.events[key].shortcut) ? block.events[key].shortcut : [
|
|
@@ -175,6 +340,16 @@ function buildEvents(block, pageContext) {
|
|
|
175
340
|
configKey: eventConfigKey
|
|
176
341
|
});
|
|
177
342
|
}
|
|
343
|
+
// The client's shortcut manager keys a plain object by the normalized
|
|
344
|
+
// shortcut. Normalization neither creates nor removes a reserved name:
|
|
345
|
+
// multi-character key names pass through unchanged, and no reserved
|
|
346
|
+
// name is a single character. So checking the raw string is enough, and
|
|
347
|
+
// a modified form like "Ctrl+__proto__" stays valid.
|
|
348
|
+
if (isReserved(shortcut)) {
|
|
349
|
+
throw new ConfigError(`Event shortcut "${shortcut}" on event "${key}" on block "${block.blockId}" on page "${pageContext.pageId}" is a reserved name and cannot be used as a shortcut.`, {
|
|
350
|
+
configKey: eventConfigKey
|
|
351
|
+
});
|
|
352
|
+
}
|
|
178
353
|
if (BROWSER_DEFAULT_SHORTCUTS.has(shortcut.toLowerCase())) {
|
|
179
354
|
pageContext.context.handleWarning(new ConfigWarning(`Shortcut "${shortcut}" on event "${key}" on block "${block.blockId}" on page "${pageContext.pageId}" conflicts with a browser default.`, {
|
|
180
355
|
configKey: eventConfigKey
|
|
@@ -75,6 +75,15 @@ function buildRequest(request, pageContext) {
|
|
|
75
75
|
pageContext.requests.push(request);
|
|
76
76
|
}
|
|
77
77
|
function buildRequests(block, pageContext) {
|
|
78
|
+
// Runtime-resolved dynamic content cannot define requests — request
|
|
79
|
+
// artifacts are written at build time, so a runtime request would have no
|
|
80
|
+
// server-side properties to execute.
|
|
81
|
+
if (pageContext.forbidRequests === true && !type.isNone(block.requests)) {
|
|
82
|
+
throw new ConfigError(`Dynamic content must not define requests — found "requests" on block "${block.blockId}" on page "${pageContext.pageId}". Reference requests defined statically on the page instead.`, {
|
|
83
|
+
received: block.requests,
|
|
84
|
+
configKey: block['~k']
|
|
85
|
+
});
|
|
86
|
+
}
|
|
78
87
|
(block.requests || []).forEach((request)=>{
|
|
79
88
|
buildRequest(request, pageContext);
|
|
80
89
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { type } from '@lowdefy/helpers';
|
|
15
|
+
*/ import { getOperatorType, type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
const breakpointKeys = new Set([
|
|
18
18
|
'xs',
|
|
@@ -22,17 +22,8 @@ const breakpointKeys = new Set([
|
|
|
22
22
|
'xl',
|
|
23
23
|
'2xl'
|
|
24
24
|
]);
|
|
25
|
-
// Keys that look like operators (single key starting with _) but are not.
|
|
26
|
-
const KNOWN_NON_OPERATORS = new Set([
|
|
27
|
-
'_id'
|
|
28
|
-
]);
|
|
29
25
|
function isOperator(value) {
|
|
30
|
-
|
|
31
|
-
if (nonTildeKeys.length !== 1) return false;
|
|
32
|
-
const key = nonTildeKeys[0];
|
|
33
|
-
const [op] = key.split('.');
|
|
34
|
-
const operator = op.replace(/^(_+)/gm, '_');
|
|
35
|
-
return operator.length > 1 && operator[0] === '_' && !KNOWN_NON_OPERATORS.has(operator);
|
|
26
|
+
return getOperatorType(value) !== null;
|
|
36
27
|
}
|
|
37
28
|
function stripDotPrefix(key) {
|
|
38
29
|
return key.startsWith('.') ? key.slice(1) : key;
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
-
function setBlockId(block, { pageId, blockIdCounter }) {
|
|
16
|
+
function setBlockId(block, { pageId, blockIdCounter, blockIdPrefix }) {
|
|
17
17
|
block.blockId = block.id;
|
|
18
18
|
if (block.blockId === pageId && blockIdCounter.getCount(block.blockId) > 0) {
|
|
19
19
|
throw new ConfigError(`Block id "${block.blockId}" on page "${pageId}" collides with the page id. A block cannot have the same id as its page.`, {
|
|
20
20
|
configKey: block['~k']
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// blockIdPrefix namespaces runtime-built dynamic content ids under the
|
|
24
|
+
// resolving Dynamic block's id so they can never collide with static ids.
|
|
25
|
+
const prefix = blockIdPrefix ?? `block:${pageId}`;
|
|
26
|
+
block.id = `${prefix}:${block.blockId}:${blockIdCounter.getCount(block.blockId)}`;
|
|
24
27
|
blockIdCounter.increment(block.blockId);
|
|
25
28
|
}
|
|
26
29
|
export default setBlockId;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { type } from '@lowdefy/helpers';
|
|
15
|
+
*/ import { isReserved, splitPath, type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
function validateBlock(block, { pageId }, parentConfigKey) {
|
|
18
18
|
const configKey = block?.['~k'] ?? parentConfigKey;
|
|
@@ -33,6 +33,17 @@ function validateBlock(block, { pageId }, parentConfigKey) {
|
|
|
33
33
|
configKey
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
// Block ids are dot-paths that nest state, so they bypass validateId's shape gate. Every segment
|
|
37
|
+
// still becomes an object key on the way through get/set, so check per segment: a block
|
|
38
|
+
// `id: a.constructor.b` would reach `get` with a reserved segment and throw mid-render, which
|
|
39
|
+
// ErrorBoundary cannot catch because the throw happens during the render that would mount it.
|
|
40
|
+
const reservedSegment = splitPath(block.id).find(isReserved);
|
|
41
|
+
if (!type.isNone(reservedSegment)) {
|
|
42
|
+
throw new ConfigError(`Block id "${block.id}" uses reserved name "${reservedSegment}" as a path segment, at page "${pageId}".`, {
|
|
43
|
+
received: block.id,
|
|
44
|
+
configKey
|
|
45
|
+
});
|
|
46
|
+
}
|
|
36
47
|
if (type.isNone(block.type)) {
|
|
37
48
|
throw new ConfigError(`Block type is not defined at "${block.id}" on page "${pageId}".`, {
|
|
38
49
|
configKey
|
|
@@ -52,5 +63,12 @@ function validateBlock(block, { pageId }, parentConfigKey) {
|
|
|
52
63
|
});
|
|
53
64
|
}
|
|
54
65
|
}
|
|
66
|
+
// Page subscriptions are extracted before block building — any subscriptions
|
|
67
|
+
// key reaching here is defined on a nested block, where it has no effect.
|
|
68
|
+
if (!type.isNone(block.subscriptions)) {
|
|
69
|
+
throw new ConfigError(`Subscriptions are only allowed on the page, not on block "${block.id}" on page "${pageId}".`, {
|
|
70
|
+
configKey
|
|
71
|
+
});
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
export default validateBlock;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
17
|
import buildBlock from './buildBlock/buildBlock.js';
|
|
18
|
+
import buildSubscriptions from './buildSubscriptions.js';
|
|
18
19
|
import collectExceptions from '../../utils/collectExceptions.js';
|
|
19
20
|
import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
|
|
20
21
|
import validateId from '../../utils/validateId.js';
|
|
@@ -54,10 +55,16 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
|
|
|
54
55
|
const requests = [];
|
|
55
56
|
const requestActionRefs = [];
|
|
56
57
|
const shortcutRefs = [];
|
|
57
|
-
|
|
58
|
+
// Extract subscriptions before block building — validateBlock rejects the
|
|
59
|
+
// subscriptions key on nested blocks, so the page root must not carry it.
|
|
60
|
+
const subscriptions = page.subscriptions;
|
|
61
|
+
delete page.subscriptions;
|
|
62
|
+
const pageContext = {
|
|
58
63
|
auth: page.auth,
|
|
59
64
|
blockIdCounter: createCounter(),
|
|
60
65
|
callApiActionRefs: context.callApiActionRefs ?? [],
|
|
66
|
+
websocketActionRefs: context.websocketActionRefs ?? [],
|
|
67
|
+
dynamicBlockRefs: context.dynamicBlockRefs ?? [],
|
|
61
68
|
checkDuplicateRequestId: createCheckDuplicateId({
|
|
62
69
|
message: 'Duplicate requestId "{{ id }}" on page "{{ pageId }}".'
|
|
63
70
|
}),
|
|
@@ -68,9 +75,26 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
|
|
|
68
75
|
shortcutRefs,
|
|
69
76
|
linkActionRefs: context.linkActionRefs,
|
|
70
77
|
typeCounters: context.typeCounters
|
|
71
|
-
}
|
|
78
|
+
};
|
|
79
|
+
buildBlock(page, pageContext);
|
|
72
80
|
// set page.id since buildBlock sets id as well.
|
|
73
81
|
page.id = `page:${page.pageId}`;
|
|
82
|
+
// Flag pages with Dynamic blocks so the server can skip resolution
|
|
83
|
+
// (and the deep copy it requires) for static pages with one property read.
|
|
84
|
+
if (pageContext.hasDynamicBlocks === true) {
|
|
85
|
+
page.dynamic = true;
|
|
86
|
+
}
|
|
87
|
+
page.subscriptions = subscriptions;
|
|
88
|
+
buildSubscriptions(page, {
|
|
89
|
+
callApiActionRefs: context.callApiActionRefs ?? [],
|
|
90
|
+
context,
|
|
91
|
+
linkActionRefs: context.linkActionRefs,
|
|
92
|
+
pageId: page.pageId,
|
|
93
|
+
requestActionRefs,
|
|
94
|
+
shortcutRefs,
|
|
95
|
+
typeCounters: context.typeCounters,
|
|
96
|
+
websocketActionRefs: context.websocketActionRefs ?? []
|
|
97
|
+
});
|
|
74
98
|
// Validate that all Request actions reference defined requests
|
|
75
99
|
validateRequestReferences({
|
|
76
100
|
requestActionRefs,
|
|
@@ -79,7 +103,7 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
|
|
|
79
103
|
context
|
|
80
104
|
});
|
|
81
105
|
// Warn on duplicate shortcuts within the page
|
|
82
|
-
const seenShortcuts =
|
|
106
|
+
const seenShortcuts = Object.create(null);
|
|
83
107
|
shortcutRefs.forEach(({ shortcut, blockId, eventId, configKey })=>{
|
|
84
108
|
if (seenShortcuts[shortcut]) {
|
|
85
109
|
context.handleWarning(new ConfigWarning(`Duplicate shortcut "${shortcut}" on event "${eventId}" on block "${blockId}" on page "${page.pageId}" — already defined on block "${seenShortcuts[shortcut].blockId}".`, {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */ /*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
|
+
import buildEvents from './buildBlock/buildEvents.js';
|
|
18
|
+
import countOperators from '../../utils/countOperators.js';
|
|
19
|
+
import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
|
|
20
|
+
const SUBSCRIPTION_EVENTS = [
|
|
21
|
+
'onMessage',
|
|
22
|
+
'onSubscribe',
|
|
23
|
+
'onError'
|
|
24
|
+
];
|
|
25
|
+
const DEFAULT_MAX_MESSAGES = 100;
|
|
26
|
+
const DEFAULT_THROTTLE_RENDER = 250;
|
|
27
|
+
const MIN_THROTTLE_RENDER = 100;
|
|
28
|
+
function buildSubscription({ subscription, pageContext }) {
|
|
29
|
+
const { context, pageId } = pageContext;
|
|
30
|
+
const configKey = subscription['~k'];
|
|
31
|
+
if (type.isUndefined(subscription.websocketId)) {
|
|
32
|
+
throw new ConfigError(`Subscription websocketId missing at page "${pageId}".`, {
|
|
33
|
+
configKey
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!type.isString(subscription.websocketId)) {
|
|
37
|
+
throw new ConfigError(`Subscription websocketId is not a string at page "${pageId}".`, {
|
|
38
|
+
received: subscription.websocketId,
|
|
39
|
+
configKey
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
pageContext.checkDuplicateSubscriptionId({
|
|
43
|
+
id: subscription.websocketId,
|
|
44
|
+
configKey,
|
|
45
|
+
pageId
|
|
46
|
+
});
|
|
47
|
+
if (!context.websocketIds.has(subscription.websocketId)) {
|
|
48
|
+
throw new ConfigError(`Subscription "${subscription.websocketId}" at page "${pageId}" references a websocket which does not exist.`, {
|
|
49
|
+
configKey
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (type.isNone(subscription.payload)) {
|
|
53
|
+
subscription.payload = {};
|
|
54
|
+
}
|
|
55
|
+
if (!type.isObject(subscription.payload)) {
|
|
56
|
+
throw new ConfigError(`Subscription "${subscription.websocketId}" at page "${pageId}" payload should be an object.`, {
|
|
57
|
+
received: subscription.payload,
|
|
58
|
+
configKey
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (type.isNone(subscription.client)) {
|
|
62
|
+
subscription.client = {};
|
|
63
|
+
}
|
|
64
|
+
if (!type.isObject(subscription.client)) {
|
|
65
|
+
throw new ConfigError(`Subscription "${subscription.websocketId}" at page "${pageId}" client should be an object.`, {
|
|
66
|
+
received: subscription.client,
|
|
67
|
+
configKey
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (type.isNone(subscription.client.maxMessages)) {
|
|
71
|
+
subscription.client.maxMessages = DEFAULT_MAX_MESSAGES;
|
|
72
|
+
}
|
|
73
|
+
if (!type.isInt(subscription.client.maxMessages) || subscription.client.maxMessages < 1) {
|
|
74
|
+
throw new ConfigError(`Subscription "${subscription.websocketId}" at page "${pageId}" client.maxMessages should be a positive integer.`, {
|
|
75
|
+
received: subscription.client.maxMessages,
|
|
76
|
+
configKey
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (type.isNone(subscription.client.throttleRender)) {
|
|
80
|
+
subscription.client.throttleRender = DEFAULT_THROTTLE_RENDER;
|
|
81
|
+
}
|
|
82
|
+
if (!type.isNumber(subscription.client.throttleRender)) {
|
|
83
|
+
throw new ConfigError(`Subscription "${subscription.websocketId}" at page "${pageId}" client.throttleRender should be a number.`, {
|
|
84
|
+
received: subscription.client.throttleRender,
|
|
85
|
+
configKey
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (subscription.client.throttleRender < MIN_THROTTLE_RENDER) {
|
|
89
|
+
context.handleWarning(new ConfigWarning(`Subscription "${subscription.websocketId}" at page "${pageId}" client.throttleRender is below the minimum of ${MIN_THROTTLE_RENDER}ms and will be clamped.`, {
|
|
90
|
+
configKey
|
|
91
|
+
}));
|
|
92
|
+
subscription.client.throttleRender = MIN_THROTTLE_RENDER;
|
|
93
|
+
}
|
|
94
|
+
if (type.isNone(subscription.events)) {
|
|
95
|
+
subscription.events = {};
|
|
96
|
+
}
|
|
97
|
+
Object.keys(subscription.events).forEach((eventName)=>{
|
|
98
|
+
if (eventName.startsWith('~')) return;
|
|
99
|
+
if (!SUBSCRIPTION_EVENTS.includes(eventName)) {
|
|
100
|
+
context.handleWarning(new ConfigWarning(`Subscription "${subscription.websocketId}" at page "${pageId}" has event "${eventName}" which will never fire. Supported events: ${SUBSCRIPTION_EVENTS.join(', ')}.`, {
|
|
101
|
+
configKey: subscription.events[eventName]?.['~k'] ?? configKey
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Reuse the block event builder — normalizes actions to { try, catch },
|
|
106
|
+
// validates action ids, counts action types, and collects Request action
|
|
107
|
+
// refs so subscription events can trigger page requests.
|
|
108
|
+
buildEvents({
|
|
109
|
+
blockId: `subscription:${subscription.websocketId}`,
|
|
110
|
+
events: subscription.events,
|
|
111
|
+
'~k': configKey
|
|
112
|
+
}, pageContext);
|
|
113
|
+
// Subscriptions are extracted from the page before buildBlock runs, so
|
|
114
|
+
// countBlockOperators never sees them — count client operators here.
|
|
115
|
+
// Payload and event action params are both evaluated client-side.
|
|
116
|
+
countOperators({
|
|
117
|
+
events: subscription.events,
|
|
118
|
+
payload: subscription.payload
|
|
119
|
+
}, {
|
|
120
|
+
counter: pageContext.typeCounters.operators.client
|
|
121
|
+
});
|
|
122
|
+
subscription.pageId = pageId;
|
|
123
|
+
subscription.id = `subscription:${pageId}:${subscription.websocketId}`;
|
|
124
|
+
}
|
|
125
|
+
function buildSubscriptions(page, pageContext) {
|
|
126
|
+
if (type.isNone(page.subscriptions)) {
|
|
127
|
+
page.subscriptions = [];
|
|
128
|
+
}
|
|
129
|
+
if (!type.isArray(page.subscriptions)) {
|
|
130
|
+
throw new ConfigError(`Subscriptions is not an array at page "${pageContext.pageId}".`, {
|
|
131
|
+
received: page.subscriptions,
|
|
132
|
+
configKey: page['~k']
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
pageContext.checkDuplicateSubscriptionId = createCheckDuplicateId({
|
|
136
|
+
message: 'Duplicate subscription "{{ id }}" on page "{{ pageId }}".'
|
|
137
|
+
});
|
|
138
|
+
page.subscriptions.forEach((subscription)=>{
|
|
139
|
+
buildSubscription({
|
|
140
|
+
subscription,
|
|
141
|
+
pageContext
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export default buildSubscriptions;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ConfigWarning } from '@lowdefy/errors';
|
|
16
|
+
function validateDynamicBlockRefs({ dynamicBlockRefs, endpointConfigs, context }) {
|
|
17
|
+
// Both Api and InternalApi endpoints are valid — resolution is an in-process
|
|
18
|
+
// call, so InternalApi is the recommended resolver type.
|
|
19
|
+
const existingEndpointIds = new Set(endpointConfigs.map((config)=>config.endpointId));
|
|
20
|
+
dynamicBlockRefs.forEach(({ endpointId, block, sourcePageId })=>{
|
|
21
|
+
if (!existingEndpointIds.has(endpointId)) {
|
|
22
|
+
context.handleWarning(new ConfigWarning(`Dynamic block "${block.blockId}" on page "${sourcePageId}" references non-existent endpoint "${endpointId}".`, {
|
|
23
|
+
configKey: block['~k'],
|
|
24
|
+
prodError: true,
|
|
25
|
+
checkSlug: 'dynamic-endpoint-refs'
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export default validateDynamicBlockRefs;
|
|
@@ -68,10 +68,12 @@ function validateStateReferences({ page, context }) {
|
|
|
68
68
|
stateRefs.forEach((configKey, topLevelKey)=>{
|
|
69
69
|
// Skip if state key is from an input block or SetState action
|
|
70
70
|
if (blockIds.has(topLevelKey) || setStateKeys.has(topLevelKey)) return;
|
|
71
|
-
const message = `_state references "${topLevelKey}" on page "${page.pageId}", ` + `but no input block with id "${topLevelKey}" exists on this page. ` + `State keys are created from input block ids. ` + `Check for typos, add an input block with this id, or initialize the state with SetState.`;
|
|
71
|
+
const message = `_state references "${topLevelKey}" on page "${page.pageId}", ` + `but no input block with id "${topLevelKey}" exists on this page. ` + `State keys are created from input block ids. ` + `Check for typos, add an input block with this id, or initialize the state with SetState. ` + `If the state is set at runtime (for example by a custom action calling setState), ` + `suppress this check with "~ignoreBuildChecks: [state-refs]" on the reference.`;
|
|
72
|
+
// This check is a heuristic: state written at runtime — custom actions
|
|
73
|
+
// calling setState, dynamic SetState wrappers — is statically invisible,
|
|
74
|
+
// so a miss here must never fail a production build (no prodError).
|
|
72
75
|
context.handleWarning(new ConfigWarning(message, {
|
|
73
76
|
configKey,
|
|
74
|
-
prodError: true,
|
|
75
77
|
checkSlug: 'state-refs'
|
|
76
78
|
}));
|
|
77
79
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ConfigWarning } from '@lowdefy/errors';
|
|
16
|
+
function validateWebsocketRefs({ websocketActionRefs, websocketIds, context }) {
|
|
17
|
+
websocketActionRefs.forEach(({ websocketId, action, actionType, sourcePageId })=>{
|
|
18
|
+
if (action.skip === true) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!websocketIds.has(websocketId)) {
|
|
22
|
+
context.handleWarning(new ConfigWarning(`${actionType} action on page "${sourcePageId}" references non-existent websocket "${websocketId}".`, {
|
|
23
|
+
configKey: action['~k'],
|
|
24
|
+
prodError: true,
|
|
25
|
+
checkSlug: 'websocket-refs'
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export default validateWebsocketRefs;
|