@lowdefy/build 5.6.0 → 6.1.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 +61 -16
- 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 +40 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +138 -0
- package/dist/build/buildAuth/buildAgentAuth.js +14 -1
- package/dist/build/buildAuth/buildAuth.js +8 -3
- 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/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- package/dist/build/buildEnvironments.js +261 -0
- 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/buildLogger.js +22 -6
- 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 +186 -21
- 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 +7 -0
- 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/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 +471 -207
- 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/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 +84 -3
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +125 -24
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +6 -1
- package/dist/build/validateEnvironmentGuards.js +69 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +98 -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 -10
- 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 +688 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +81 -10
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +969 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +15 -1
- package/dist/test-utils/runBuildForSnapshots.js +37 -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/getEnvironmentNames.js +26 -0
- package/dist/utils/logCollectedErrors.js +15 -9
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +67 -60
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
|
@@ -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 : [
|
|
@@ -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;
|
|
@@ -63,5 +63,12 @@ function validateBlock(block, { pageId }, parentConfigKey) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
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
|
+
}
|
|
66
73
|
}
|
|
67
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;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import { resolve, WalkContext } from './walker.js';
|
|
17
17
|
import getRefContent from './getRefContent.js';
|
|
18
18
|
import makeRefDefinition from './makeRefDefinition.js';
|
|
19
|
-
import evaluateStaticOperators from './evaluateStaticOperators.js';
|
|
20
19
|
import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
|
|
21
20
|
import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
|
|
22
21
|
import isPageContentPath from '../jit/isPageContentPath.js';
|
|
@@ -30,6 +29,9 @@ const dynamicIdentifiers = collectDynamicIdentifiers({
|
|
|
30
29
|
async function buildRefs({ context, shallowOptions }) {
|
|
31
30
|
context.unresolvedRefVars = context.unresolvedRefVars ?? {};
|
|
32
31
|
const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
|
|
32
|
+
// Stash for Phase 3.5 (precomputeRuntimeOperators) so it can resolve error
|
|
33
|
+
// source file paths without creating a new makeId entry.
|
|
34
|
+
context.rootRefDef = refDef;
|
|
33
35
|
const ctx = new WalkContext({
|
|
34
36
|
buildContext: context,
|
|
35
37
|
refId: refDef.id,
|
|
@@ -42,24 +44,31 @@ async function buildRefs({ context, shallowOptions }) {
|
|
|
42
44
|
] : []),
|
|
43
45
|
operators,
|
|
44
46
|
env: process.env,
|
|
47
|
+
lowdefyApp: context.appMeta,
|
|
45
48
|
dynamicIdentifiers,
|
|
46
|
-
shouldStop:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
shouldStop: (path, refId)=>{
|
|
50
|
+
// Module entry vars/connections were already resolved by parseLowdefyYaml
|
|
51
|
+
// (same predicate) and live on context.modules — preserve the raw blobs so
|
|
52
|
+
// the app pass doesn't pull and walk their refs a second time. buildModules
|
|
53
|
+
// reads entry ids only and deletes components.modules before testSchema.
|
|
54
|
+
if (/^modules\.\d+\.vars$/.test(path)) return 'preserve';
|
|
55
|
+
if (/^modules\.\d+\.connections$/.test(path)) return 'preserve';
|
|
56
|
+
if (shallowOptions) {
|
|
57
|
+
// Strip page content (blocks, events, etc.) from ref-backed pages so
|
|
58
|
+
// JIT can re-resolve them from source files. Inline pages (defined
|
|
59
|
+
// directly in lowdefy.yaml) live in the root ref and have no separate
|
|
60
|
+
// source file — their content must be preserved for buildShallowPages.
|
|
61
|
+
return isPageContentPath(path) && refId !== refDef.id;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
50
65
|
});
|
|
51
66
|
const content = await getRefContent({
|
|
52
67
|
context,
|
|
53
68
|
refDef,
|
|
54
69
|
referencedFrom: null
|
|
55
70
|
});
|
|
56
|
-
|
|
57
|
-
// Evaluate static operators (_sum, _if, etc.) that don't depend on runtime data
|
|
58
|
-
components = evaluateStaticOperators({
|
|
59
|
-
context,
|
|
60
|
-
input: components,
|
|
61
|
-
refDef
|
|
62
|
-
});
|
|
71
|
+
const components = await resolve(content, ctx);
|
|
63
72
|
return components ?? {};
|
|
64
73
|
}
|
|
65
74
|
export default buildRefs;
|