@lowdefy/build 5.6.0 → 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 +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 +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 +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/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 +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 +81 -1
- 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 +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/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 +666 -388
- 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 +12 -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 +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 +66 -59
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
|
@@ -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;
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
16
|
+
import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
|
|
17
|
+
// THE deep clone for config subtrees inside buildRefs — preserves the
|
|
18
|
+
// non-enumerable provenance markers (~r ~l ~k ~arr), and with assignRefId set
|
|
19
|
+
// stamps ~r on nodes that have none (pass nothing to preserve the template's
|
|
20
|
+
// existing markers only). Non-plain values (Date) pass by reference; nothing
|
|
21
|
+
// in the build mutates them.
|
|
22
|
+
//
|
|
23
|
+
// Do not use serializer.copy on config subtrees in buildRefs: it round-trips
|
|
24
|
+
// the same markers but pays a JSON round-trip and re-instantiates Dates — the
|
|
25
|
+
// serializer is the artifact boundary, this is the in-memory clone.
|
|
26
|
+
function cloneWithMarkers(value, { assignRefId = null } = {}) {
|
|
27
|
+
if (!type.isObject(value) && !type.isArray(value)) return value;
|
|
28
|
+
let clone;
|
|
29
|
+
if (type.isArray(value)) {
|
|
30
|
+
clone = value.map((item)=>cloneWithMarkers(item, {
|
|
31
|
+
assignRefId
|
|
32
|
+
}));
|
|
33
|
+
} else {
|
|
34
|
+
clone = {};
|
|
35
|
+
for (const key of Object.keys(value)){
|
|
36
|
+
clone[key] = cloneWithMarkers(value[key], {
|
|
37
|
+
assignRefId
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (value['~r'] !== undefined) {
|
|
42
|
+
setNonEnumerableProperty(clone, '~r', value['~r']);
|
|
43
|
+
} else if (assignRefId) {
|
|
44
|
+
setNonEnumerableProperty(clone, '~r', assignRefId);
|
|
45
|
+
}
|
|
46
|
+
for (const marker of [
|
|
47
|
+
'~l',
|
|
48
|
+
'~k',
|
|
49
|
+
'~arr'
|
|
50
|
+
]){
|
|
51
|
+
if (value[marker] !== undefined) {
|
|
52
|
+
setNonEnumerableProperty(clone, marker, value[marker]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return clone;
|
|
56
|
+
}
|
|
57
|
+
export default cloneWithMarkers;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
*/ // The single source of truth for which module-manifest regions each build
|
|
16
|
+
// phase defers, records, or skips — one table instead of hand-maintained
|
|
17
|
+
// shouldStop regex sets that must agree.
|
|
18
|
+
//
|
|
19
|
+
// Phases:
|
|
20
|
+
// - 'header' Phase A: parse static header keys (dependencies, plugins,
|
|
21
|
+
// secrets, vars definitions — defaults record-ified); content
|
|
22
|
+
// sections stay raw parsed YAML ('skip' = leave raw, do not
|
|
23
|
+
// descend).
|
|
24
|
+
// - 'exportables' Phase C.5: record-ify per-consumer bodies (components,
|
|
25
|
+
// menu links) in module-static scope; everything else stays
|
|
26
|
+
// raw for Phase D.
|
|
27
|
+
// - 'manifest' Phase D: one full walk; only varDefault placeholders need a
|
|
28
|
+
// rule (single-value kinds would otherwise be forced by the
|
|
29
|
+
// placeholder dispatch — they are demand-only). Per-consumer
|
|
30
|
+
// placeholders pass through the dispatch untouched.
|
|
31
|
+
//
|
|
32
|
+
// entryRef/connRemap creation is value-shape-driven (deferModuleRefs in the
|
|
33
|
+
// walker), not path-driven, so it has no row here. The JIT page-content
|
|
34
|
+
// predicate lives in buildRefs.js (it composes with the app-pass modules
|
|
35
|
+
// preserve and is not a manifest phase).
|
|
36
|
+
const VAR_DEFAULT = /^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/;
|
|
37
|
+
const CONTENT_SECTIONS = /^(pages|api|connections|agents|notifications|menus|components)(\..*)?$/;
|
|
38
|
+
const NON_EXPORTABLE_SECTIONS = /^(pages|api|connections|agents|notifications|vars|dependencies|plugins|secrets)(\..*)?$/;
|
|
39
|
+
const DEFERRED_REGIONS = [
|
|
40
|
+
{
|
|
41
|
+
phase: 'header',
|
|
42
|
+
match: VAR_DEFAULT,
|
|
43
|
+
action: 'record:varDefault'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
phase: 'header',
|
|
47
|
+
match: CONTENT_SECTIONS,
|
|
48
|
+
action: 'skip'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
phase: 'exportables',
|
|
52
|
+
match: /^components\.\d+\.component$/,
|
|
53
|
+
action: 'record:component'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
phase: 'exportables',
|
|
57
|
+
match: /^menus\.\d+\.links$/,
|
|
58
|
+
action: 'record:menuLinks'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
phase: 'exportables',
|
|
62
|
+
match: NON_EXPORTABLE_SECTIONS,
|
|
63
|
+
action: 'skip'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
phase: 'manifest',
|
|
67
|
+
match: VAR_DEFAULT,
|
|
68
|
+
action: 'record:varDefault'
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
function makeShouldStop(phase) {
|
|
72
|
+
const rows = DEFERRED_REGIONS.filter((row)=>row.phase === phase);
|
|
73
|
+
return (childPath)=>{
|
|
74
|
+
for (const row of rows){
|
|
75
|
+
if (row.match.test(childPath)) return row.action;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export { DEFERRED_REGIONS, makeShouldStop };
|