@lowdefy/build 5.2.0 → 5.4.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 +249 -0
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +6 -2
- package/dist/build/buildApi/buildRoutine/validateStep.js +18 -0
- package/dist/build/buildApp.js +18 -5
- package/dist/build/buildImports/buildImportsDev.js +5 -0
- package/dist/build/buildImports/buildImportsProd.js +1 -0
- package/dist/build/buildJs/writeJs.js +2 -2
- package/dist/build/buildMenu.js +5 -0
- package/dist/build/buildModuleDefs.js +52 -1
- package/dist/build/buildModules.js +1 -1
- package/dist/build/buildPages/validateCallApiRefs.js +9 -0
- package/dist/build/buildRefs/getUserJavascriptFunction.js +6 -9
- package/dist/build/buildRefs/runTransformer.js +3 -2
- package/dist/build/buildRefs/walker.js +22 -39
- package/dist/build/buildTypes.js +7 -0
- package/dist/build/codegenI18nLocales.js +53 -0
- package/dist/build/copyAgentFileSystems.js +45 -0
- package/dist/build/full/updateServerPackageJson.js +1 -0
- package/dist/build/jit/shallowBuild.js +31 -0
- package/dist/build/registerModules.js +11 -33
- package/dist/build/writeAgents.js +26 -0
- package/dist/build/writeAppMeta.js +19 -0
- package/dist/build/writeI18n.js +117 -0
- package/dist/build/writePluginImports/writeAgentImports.js +22 -0
- package/dist/build/writePluginImports/writePluginImports.js +10 -0
- package/dist/build/writePluginImports/writeServerExternalPackages.js +121 -0
- package/dist/createContext.js +8 -1
- package/dist/defaultMessagesMap.js +3 -0
- package/dist/defaultPackages.js +7 -0
- package/dist/defaultTypesMap.js +590 -377
- package/dist/index.js +30 -0
- package/dist/lowdefySchema.js +414 -0
- package/dist/scripts/generateDefaultMessages.js +37 -0
- package/dist/scripts/generateDefaultTypes.js +1 -0
- package/dist/test-utils/testContext.js +2 -0
- package/dist/utils/createPluginTypesMap.js +7 -0
- package/package.json +51 -44
|
@@ -0,0 +1,249 @@
|
|
|
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 path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
19
|
+
import { RESERVED_PLATFORM_TOOL_NAMES } from '@lowdefy/ai-utils';
|
|
20
|
+
import countOperators from '../utils/countOperators.js';
|
|
21
|
+
import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
|
|
22
|
+
function detectCycles(agents) {
|
|
23
|
+
const graph = {};
|
|
24
|
+
for (const agent of agents){
|
|
25
|
+
graph[agent.agentId] = (agent.agents ?? []).map((ref)=>ref.agentId);
|
|
26
|
+
}
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
const inStack = new Set();
|
|
29
|
+
function dfs(id) {
|
|
30
|
+
if (inStack.has(id)) return id;
|
|
31
|
+
if (visited.has(id)) return null;
|
|
32
|
+
visited.add(id);
|
|
33
|
+
inStack.add(id);
|
|
34
|
+
for (const neighbor of graph[id] ?? []){
|
|
35
|
+
const cycleNode = dfs(neighbor);
|
|
36
|
+
if (cycleNode !== null) return cycleNode;
|
|
37
|
+
}
|
|
38
|
+
inStack.delete(id);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
for (const id of Object.keys(graph)){
|
|
42
|
+
const cycleNode = dfs(id);
|
|
43
|
+
if (cycleNode !== null) {
|
|
44
|
+
return cycleNode;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function buildAgents({ components, context }) {
|
|
50
|
+
if (!type.isArray(components.agents)) {
|
|
51
|
+
return components;
|
|
52
|
+
}
|
|
53
|
+
context.agentIds = new Set();
|
|
54
|
+
const checkDuplicateAgentId = createCheckDuplicateId({
|
|
55
|
+
message: 'Duplicate agentId "{{ id }}".'
|
|
56
|
+
});
|
|
57
|
+
components.agents.forEach((agent)=>{
|
|
58
|
+
const configKey = agent['~k'];
|
|
59
|
+
// Check duplicates
|
|
60
|
+
checkDuplicateAgentId({
|
|
61
|
+
id: agent.id,
|
|
62
|
+
configKey
|
|
63
|
+
});
|
|
64
|
+
// Track type usage for buildTypes validation
|
|
65
|
+
context.typeCounters.agents.increment(agent.type, configKey);
|
|
66
|
+
// Validate connectionId is provided
|
|
67
|
+
if (type.isNone(agent.connectionId)) {
|
|
68
|
+
throw new ConfigError(`Agent connectionId is not defined at "${agent.id}".`, {
|
|
69
|
+
configKey
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Validate connectionId references an existing connection
|
|
73
|
+
// Connections may have been renamed by buildConnections:
|
|
74
|
+
// connection.connectionId = original id, connection.id = 'connection:' + original id
|
|
75
|
+
const connectionExists = (components.connections ?? []).some((c)=>c.id === agent.connectionId || c.connectionId === agent.connectionId);
|
|
76
|
+
if (!connectionExists) {
|
|
77
|
+
throw new ConfigError(`Agent "${agent.id}" references connectionId "${agent.connectionId}" which does not exist.`, {
|
|
78
|
+
configKey
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Validate model is defined
|
|
82
|
+
if (type.isNone(agent.properties?.model)) {
|
|
83
|
+
throw new ConfigError(`Agent "model" is not defined at "${agent.id}".`, {
|
|
84
|
+
configKey
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Normalize tool strings to objects
|
|
88
|
+
agent.tools = (agent.tools ?? []).map((tool)=>{
|
|
89
|
+
if (type.isString(tool)) {
|
|
90
|
+
return {
|
|
91
|
+
endpointId: tool
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return tool;
|
|
95
|
+
});
|
|
96
|
+
// Validate tools reference existing API endpoints with required tool metadata
|
|
97
|
+
agent.tools.forEach((toolConfig)=>{
|
|
98
|
+
if (RESERVED_PLATFORM_TOOL_NAMES.includes(toolConfig.endpointId)) {
|
|
99
|
+
throw new ConfigError(`Agent "${agent.id}" tool "${toolConfig.endpointId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
|
|
100
|
+
configKey
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === toolConfig.endpointId || e.endpointId === toolConfig.endpointId);
|
|
104
|
+
if (!endpoint) {
|
|
105
|
+
throw new ConfigError(`Agent "${agent.id}" references tool endpoint "${toolConfig.endpointId}" which does not exist.`, {
|
|
106
|
+
configKey
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (type.isNone(endpoint.description)) {
|
|
110
|
+
throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "description".`, {
|
|
111
|
+
configKey: endpoint['~k']
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (type.isNone(endpoint.payloadSchema)) {
|
|
115
|
+
throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "payloadSchema".`, {
|
|
116
|
+
configKey: endpoint['~k']
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// Normalize MCP string shorthand to connectionId objects (same pattern as tools)
|
|
121
|
+
agent.mcp = (agent.mcp ?? []).map((mcp)=>{
|
|
122
|
+
if (type.isString(mcp)) {
|
|
123
|
+
return {
|
|
124
|
+
connectionId: mcp
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return mcp;
|
|
128
|
+
});
|
|
129
|
+
// Validate MCP sources
|
|
130
|
+
agent.mcp.forEach((mcpSource, index)=>{
|
|
131
|
+
if (!type.isNone(mcpSource.connectionId)) {
|
|
132
|
+
// Validate connectionId references an existing connection
|
|
133
|
+
const mcpConnectionExists = (components.connections ?? []).some((c)=>c.id === mcpSource.connectionId || c.connectionId === mcpSource.connectionId);
|
|
134
|
+
if (!mcpConnectionExists) {
|
|
135
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} references connection "${mcpSource.connectionId}" which does not exist.`, {
|
|
136
|
+
configKey
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} else if (mcpSource.transport === 'stdio') {
|
|
140
|
+
if (type.isNone(mcpSource.command)) {
|
|
141
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} uses stdio transport but is missing "command".`, {
|
|
142
|
+
configKey
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
if (type.isNone(mcpSource.url)) {
|
|
147
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} is missing "url".`, {
|
|
148
|
+
configKey
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Validate hooks reference existing API endpoints
|
|
154
|
+
const hookNames = [
|
|
155
|
+
'onStart',
|
|
156
|
+
'onStepStart',
|
|
157
|
+
'onToolCallStart',
|
|
158
|
+
'onToolCallFinish',
|
|
159
|
+
'onStepFinish',
|
|
160
|
+
'onFinish'
|
|
161
|
+
];
|
|
162
|
+
hookNames.forEach((hookName)=>{
|
|
163
|
+
(agent.hooks?.[hookName] ?? []).forEach((endpointId)=>{
|
|
164
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === endpointId || e.endpointId === endpointId);
|
|
165
|
+
if (!endpoint) {
|
|
166
|
+
throw new ConfigError(`Agent "${agent.id}" hook "${hookName}" references endpoint "${endpointId}" which does not exist.`, {
|
|
167
|
+
configKey
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// Normalize sub-agent strings to objects (same pattern as tools/mcp)
|
|
173
|
+
agent.agents = (agent.agents ?? []).map((ref)=>{
|
|
174
|
+
if (type.isString(ref)) {
|
|
175
|
+
return {
|
|
176
|
+
agentId: ref
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return ref;
|
|
180
|
+
});
|
|
181
|
+
// Validate fileSystem basePath if present
|
|
182
|
+
if (agent.properties?.fileSystem) {
|
|
183
|
+
const basePath = agent.properties.fileSystem.basePath;
|
|
184
|
+
if (!type.isString(basePath)) {
|
|
185
|
+
throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath is not a string.`, {
|
|
186
|
+
received: basePath,
|
|
187
|
+
configKey
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const resolved = path.resolve(context.directories.config, basePath);
|
|
191
|
+
if (!fs.existsSync(resolved)) {
|
|
192
|
+
throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath "${basePath}" does not exist.`, {
|
|
193
|
+
configKey
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Rename id to internal format
|
|
198
|
+
agent.agentId = agent.id;
|
|
199
|
+
context.agentIds.add(agent.agentId);
|
|
200
|
+
agent.id = `agent:${agent.agentId}`;
|
|
201
|
+
// Count server operators in properties
|
|
202
|
+
countOperators(agent.properties ?? {}, {
|
|
203
|
+
counter: context.typeCounters.operators.server
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// Second pass: validate sub-agent references (needs all agentIds collected)
|
|
207
|
+
components.agents.forEach((agent)=>{
|
|
208
|
+
const configKey = agent['~k'];
|
|
209
|
+
agent.agents.forEach((subAgentRef)=>{
|
|
210
|
+
// Validate sub-agent reference exists
|
|
211
|
+
if (!context.agentIds.has(subAgentRef.agentId)) {
|
|
212
|
+
throw new ConfigError(`Agent "${agent.agentId}" references sub-agent "${subAgentRef.agentId}" which does not exist.`, {
|
|
213
|
+
configKey
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Reserved platform tool name guard for sub-agents
|
|
217
|
+
if (RESERVED_PLATFORM_TOOL_NAMES.includes(subAgentRef.agentId)) {
|
|
218
|
+
throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
|
|
219
|
+
configKey
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Check for name collision with endpoint tools
|
|
223
|
+
const hasToolCollision = agent.tools.some((toolConfig)=>toolConfig.endpointId === subAgentRef.agentId);
|
|
224
|
+
if (hasToolCollision) {
|
|
225
|
+
throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" conflicts with an endpoint tool of the same name.`, {
|
|
226
|
+
configKey
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Warn if sub-agent has tools with confirm: true (unsupported in sub-agent context)
|
|
230
|
+
const subAgent = components.agents.find((a)=>a.agentId === subAgentRef.agentId);
|
|
231
|
+
const hasConfirmTools = (subAgent?.tools ?? []).some((t)=>t.confirm);
|
|
232
|
+
if (hasConfirmTools) {
|
|
233
|
+
context.handleWarning(new ConfigWarning(`Agent "${subAgentRef.agentId}" has tools with confirm: true, but tool approval is not supported in sub-agent context. Tools will auto-execute when called as a sub-agent.`, {
|
|
234
|
+
configKey
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// Detect circular sub-agent references
|
|
240
|
+
const cycleNode = detectCycles(components.agents);
|
|
241
|
+
if (cycleNode !== null) {
|
|
242
|
+
const agent = components.agents.find((a)=>a.agentId === cycleNode);
|
|
243
|
+
throw new ConfigError(`Circular sub-agent reference detected involving "${cycleNode}".`, {
|
|
244
|
+
configKey: agent?.['~k']
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return components;
|
|
248
|
+
}
|
|
249
|
+
export default buildAgents;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ function countStepTypes(step, { typeCounters }) {
|
|
16
|
-
if (step.type === 'CallApi') {
|
|
16
|
+
if (step.type === 'CallApi' || step.type === 'ValidateSchema') {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
typeCounters.requests.increment(step.type, step['~k']);
|
|
@@ -12,10 +12,14 @@
|
|
|
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
|
-
*/
|
|
15
|
+
*/ const prefixByType = {
|
|
16
|
+
CallApi: 'endpoint',
|
|
17
|
+
ValidateSchema: 'validate'
|
|
18
|
+
};
|
|
19
|
+
function setStepId(step, { endpointId }) {
|
|
16
20
|
step.stepId = step.id;
|
|
17
21
|
step.endpointId = endpointId;
|
|
18
|
-
const prefix = step.type
|
|
22
|
+
const prefix = prefixByType[step.type] ?? 'request';
|
|
19
23
|
step.id = `${prefix}:${endpointId}:${step.stepId}`;
|
|
20
24
|
}
|
|
21
25
|
export default setStepId;
|
|
@@ -69,6 +69,24 @@ function validateStep(step, { endpointId }) {
|
|
|
69
69
|
}
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
+
if (step.type === 'ValidateSchema') {
|
|
73
|
+
if (type.isNone(step.properties?.schema)) {
|
|
74
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" requires properties.schema.`, {
|
|
75
|
+
configKey
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (type.isNone(step.properties?.data)) {
|
|
79
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" requires properties.data.`, {
|
|
80
|
+
configKey
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!type.isNone(step.connectionId)) {
|
|
84
|
+
throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
|
|
85
|
+
configKey
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
72
90
|
if (type.isUndefined(step.connectionId)) {
|
|
73
91
|
throw new ConfigError(`Step connectionId missing at endpoint "${endpointId}".`, {
|
|
74
92
|
configKey
|
package/dist/build/buildApp.js
CHANGED
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { execSync } from 'child_process';
|
|
16
16
|
import { type } from '@lowdefy/helpers';
|
|
17
|
+
function computeGitSha() {
|
|
18
|
+
const fromEnv = process.env.LOWDEFY_GIT_SHA?.trim();
|
|
19
|
+
if (fromEnv) return fromEnv;
|
|
20
|
+
try {
|
|
21
|
+
return execSync('git rev-parse HEAD').toString().trim();
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
function buildApp({ components }) {
|
|
18
27
|
if (type.isNone(components.app)) {
|
|
19
28
|
components.app = {};
|
|
@@ -30,11 +39,15 @@ function buildApp({ components }) {
|
|
|
30
39
|
if (type.isNone(components.app.html.appendHead)) {
|
|
31
40
|
components.app.html.appendHead = '';
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
components.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
components.appMeta = {
|
|
43
|
+
slug: components.slug ?? null,
|
|
44
|
+
name: components.name ?? null,
|
|
45
|
+
version: components.version ?? null,
|
|
46
|
+
description: components.description ?? null,
|
|
47
|
+
license: components.license ?? null,
|
|
48
|
+
lowdefyVersion: components.lowdefy ?? null,
|
|
49
|
+
gitSha: computeGitSha()
|
|
50
|
+
};
|
|
38
51
|
return components;
|
|
39
52
|
}
|
|
40
53
|
export default buildApp;
|
|
@@ -22,6 +22,7 @@ function getPluginPackages({ components }) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
getPackages(components.types.actions);
|
|
25
|
+
getPackages(components.types.agents);
|
|
25
26
|
getPackages(components.types.auth.adapters);
|
|
26
27
|
getPackages(components.types.auth.callbacks);
|
|
27
28
|
getPackages(components.types.auth.events);
|
|
@@ -53,6 +54,10 @@ function buildImportsDev({ components, context }) {
|
|
|
53
54
|
pluginPackages,
|
|
54
55
|
map: context.typesMap.actions
|
|
55
56
|
}),
|
|
57
|
+
agents: buildImportClassDev({
|
|
58
|
+
pluginPackages,
|
|
59
|
+
map: context.typesMap.agents
|
|
60
|
+
}),
|
|
56
61
|
auth: {
|
|
57
62
|
adapters: buildImportClassDev({
|
|
58
63
|
pluginPackages,
|
|
@@ -25,6 +25,7 @@ function buildImportsProd({ components, context }) {
|
|
|
25
25
|
const blocks = buildImportClassProd(components.types.blocks);
|
|
26
26
|
return {
|
|
27
27
|
actions: buildImportClassProd(components.types.actions),
|
|
28
|
+
agents: buildImportClassProd(components.types.agents),
|
|
28
29
|
auth: {
|
|
29
30
|
adapters: buildImportClassProd(components.types.auth.adapters),
|
|
30
31
|
callbacks: buildImportClassProd(components.types.auth.callbacks),
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
async function writeJs({ context }) {
|
|
17
17
|
await context.writeBuildArtifact('plugins/operators/clientJsMap.js', generateJsFile({
|
|
18
18
|
map: context.jsMap.client,
|
|
19
|
-
functionPrototype: `{ actions, args, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
|
|
19
|
+
functionPrototype: `{ actions, args, event, input, location, lowdefyApp, lowdefyGlobal, request, state, urlQuery, user }`
|
|
20
20
|
}));
|
|
21
21
|
await context.writeBuildArtifact('plugins/operators/serverJsMap.js', generateJsFile({
|
|
22
22
|
map: context.jsMap.server,
|
|
23
|
-
functionPrototype: `{ args, item, payload,
|
|
23
|
+
functionPrototype: `{ args, item, lowdefyApp, payload, secret, state, step, user }`
|
|
24
24
|
}));
|
|
25
25
|
}
|
|
26
26
|
export default writeJs;
|
package/dist/build/buildMenu.js
CHANGED
|
@@ -103,6 +103,11 @@ function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateM
|
|
|
103
103
|
public: true
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
if (menuItem.type === 'MenuDivider') {
|
|
107
|
+
menuItem.auth = {
|
|
108
|
+
public: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
106
111
|
checkDuplicateMenuItemId({
|
|
107
112
|
id: menuItem.id,
|
|
108
113
|
menuId,
|
|
@@ -20,7 +20,7 @@ import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
|
20
20
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
21
21
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
22
22
|
import fetchModules from './fetchModules.js';
|
|
23
|
-
import { resolveLocalManifest, resolveFullManifest } from './registerModules.js';
|
|
23
|
+
import { resolveLocalManifest, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
24
24
|
import resolveModuleDependencies from './resolveModuleDependencies.js';
|
|
25
25
|
validateOperatorsDynamic({
|
|
26
26
|
operators
|
|
@@ -30,6 +30,9 @@ const dynamicIdentifiers = collectDynamicIdentifiers({
|
|
|
30
30
|
});
|
|
31
31
|
async function parseLowdefyYaml({ context }) {
|
|
32
32
|
const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
|
|
33
|
+
// Stash for Phase 2.5 — consumer vars come from lowdefy.yaml, so refs
|
|
34
|
+
// within them must be parented to lowdefy.yaml's refDef.
|
|
35
|
+
context.lowdefyYamlRefDef = refDef;
|
|
33
36
|
const content = await getRefContent({
|
|
34
37
|
context,
|
|
35
38
|
refDef,
|
|
@@ -49,6 +52,10 @@ async function parseLowdefyYaml({ context }) {
|
|
|
49
52
|
env: process.env,
|
|
50
53
|
dynamicIdentifiers,
|
|
51
54
|
shouldStop: (path)=>{
|
|
55
|
+
// Defer entry vars and connections: they may contain cross-module
|
|
56
|
+
// refs that require modules to be registered first.
|
|
57
|
+
if (/^modules\.\d+\.vars$/.test(path)) return 'preserve';
|
|
58
|
+
if (/^modules\.\d+\.connections$/.test(path)) return 'preserve';
|
|
52
59
|
if (path.startsWith('modules')) return false;
|
|
53
60
|
return 'preserve';
|
|
54
61
|
}
|
|
@@ -61,6 +68,42 @@ async function parseLowdefyYaml({ context }) {
|
|
|
61
68
|
});
|
|
62
69
|
return config ?? {};
|
|
63
70
|
}
|
|
71
|
+
async function resolveEntryConfig({ entry, context }) {
|
|
72
|
+
const moduleEntry = context.modules[entry.id];
|
|
73
|
+
const lowdefyYamlRefDef = context.lowdefyYamlRefDef;
|
|
74
|
+
function makeAppLevelCtx() {
|
|
75
|
+
return new WalkContext({
|
|
76
|
+
buildContext: context,
|
|
77
|
+
refId: lowdefyYamlRefDef.id,
|
|
78
|
+
sourceRefId: null,
|
|
79
|
+
vars: {},
|
|
80
|
+
path: '',
|
|
81
|
+
currentFile: lowdefyYamlRefDef.path,
|
|
82
|
+
refChain: new Set(lowdefyYamlRefDef.path ? [
|
|
83
|
+
lowdefyYamlRefDef.path
|
|
84
|
+
] : []),
|
|
85
|
+
operators,
|
|
86
|
+
env: process.env,
|
|
87
|
+
dynamicIdentifiers
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const refDef = lowdefyYamlRefDef;
|
|
91
|
+
let resolvedVars = await resolve(moduleEntry.consumerVars, makeAppLevelCtx());
|
|
92
|
+
resolvedVars = evaluateStaticOperators({
|
|
93
|
+
context,
|
|
94
|
+
input: resolvedVars,
|
|
95
|
+
refDef
|
|
96
|
+
});
|
|
97
|
+
moduleEntry.consumerVars = resolvedVars ?? {};
|
|
98
|
+
let resolvedConnections = await resolve(moduleEntry.connections, makeAppLevelCtx());
|
|
99
|
+
resolvedConnections = evaluateStaticOperators({
|
|
100
|
+
context,
|
|
101
|
+
input: resolvedConnections,
|
|
102
|
+
refDef
|
|
103
|
+
});
|
|
104
|
+
moduleEntry.connections = resolvedConnections ?? {};
|
|
105
|
+
validateRequiredVars(moduleEntry.varDefs, moduleEntry.consumerVars, entry.id, entry.source);
|
|
106
|
+
}
|
|
64
107
|
async function buildModuleDefs({ context }) {
|
|
65
108
|
const lowdefyConfig = await parseLowdefyYaml({
|
|
66
109
|
context
|
|
@@ -86,6 +129,14 @@ async function buildModuleDefs({ context }) {
|
|
|
86
129
|
resolveModuleDependencies({
|
|
87
130
|
context
|
|
88
131
|
});
|
|
132
|
+
// Step 2.5: Resolve deferred entry vars and connections at app level,
|
|
133
|
+
// then validate required vars against the resolved values.
|
|
134
|
+
for (const entry of moduleEntries){
|
|
135
|
+
await resolveEntryConfig({
|
|
136
|
+
entry,
|
|
137
|
+
context
|
|
138
|
+
});
|
|
139
|
+
}
|
|
89
140
|
// Step 3: Full resolve — cross-module refs, preserved content
|
|
90
141
|
for (const entryId of Object.keys(context.modules)){
|
|
91
142
|
await resolveFullManifest({
|
|
@@ -45,7 +45,7 @@ function buildModules({ components, context }) {
|
|
|
45
45
|
const moduleConnIds = new Set((manifest.connections ?? []).map((c)=>c.id));
|
|
46
46
|
for (const remapKey of Object.keys(remapping)){
|
|
47
47
|
if (!moduleConnIds.has(remapKey)) {
|
|
48
|
-
throw new ConfigError(`Module "${entry.id}" connection remapping references "${remapKey}", ` + `but the module
|
|
48
|
+
throw new ConfigError(`Module "${entry.id}" connection remapping references "${remapKey}", ` + `but the module has no connection with that id.`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// Validate secret whitelist on non-remapped content
|
|
@@ -14,11 +14,20 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigWarning } from '@lowdefy/errors';
|
|
16
16
|
function validateCallApiRefs({ callApiActionRefs, endpointConfigs, context }) {
|
|
17
|
+
const existingEndpointIds = new Set(endpointConfigs.map((config)=>config.endpointId));
|
|
17
18
|
const internalEndpoints = new Set(endpointConfigs.filter((config)=>config.type === 'InternalApi').map((config)=>config.endpointId));
|
|
18
19
|
callApiActionRefs.forEach(({ endpointId, action, sourcePageId })=>{
|
|
19
20
|
if (action.skip === true) {
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
23
|
+
if (!existingEndpointIds.has(endpointId)) {
|
|
24
|
+
context.handleWarning(new ConfigWarning(`CallAPI action on page "${sourcePageId}" references non-existent endpoint "${endpointId}".`, {
|
|
25
|
+
configKey: action['~k'],
|
|
26
|
+
prodError: true,
|
|
27
|
+
checkSlug: 'callapi-refs'
|
|
28
|
+
}));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
22
31
|
if (internalEndpoints.has(endpointId)) {
|
|
23
32
|
context.handleWarning(new ConfigWarning(`CallAPI action on page "${sourcePageId}" targets InternalApi endpoint "${endpointId}". InternalApi endpoints are not accessible from client pages.`, {
|
|
24
33
|
configKey: action['~k'],
|
|
@@ -15,20 +15,17 @@
|
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import { pathToFileURL } from 'url';
|
|
17
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
18
|
-
// Create a native import() that survives webpack bundling. When this module is
|
|
19
|
-
// bundled by Next.js webpack for server-dev API routes, webpack transforms
|
|
20
|
-
// import() into __webpack_require__() which can't handle file:// URLs for
|
|
21
|
-
// loading user-provided resolver and transformer JS files from the config
|
|
22
|
-
// directory. The Function constructor creates the import call at runtime,
|
|
23
|
-
// bypassing webpack's static analysis.
|
|
24
|
-
const nativeImport = new Function('specifier', 'return import(specifier)');
|
|
25
18
|
async function getUserJavascriptFunction({ context, filePath }) {
|
|
26
19
|
try {
|
|
27
|
-
const fileUrl = pathToFileURL(path.
|
|
20
|
+
const fileUrl = pathToFileURL(path.resolve(context.directories.config, filePath));
|
|
28
21
|
// Bust Node.js module cache so edits to resolver/transformer JS files are
|
|
29
22
|
// picked up during dev rebuilds. Each import gets a unique URL.
|
|
30
23
|
fileUrl.searchParams.set('t', Date.now());
|
|
31
|
-
|
|
24
|
+
// webpackIgnore tells Next.js webpack to leave this dynamic import alone
|
|
25
|
+
// when bundling server-dev API routes — otherwise webpack rewrites import()
|
|
26
|
+
// into __webpack_require__() which can't handle file:// URLs for loading
|
|
27
|
+
// user-provided resolver/transformer JS files from the config directory.
|
|
28
|
+
return (await import(/* webpackIgnore: true */ fileUrl.href)).default;
|
|
32
29
|
} catch (error) {
|
|
33
30
|
throw new ConfigError(`Error importing ${filePath}.`, {
|
|
34
31
|
cause: error,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
16
|
import getUserJavascriptFunction from './getUserJavascriptFunction.js';
|
|
17
|
-
async function runTransformer({ context, input, refDef }) {
|
|
17
|
+
async function runTransformer({ context, input, refDef, referencedFrom }) {
|
|
18
18
|
if (refDef.transformer) {
|
|
19
19
|
const transformerFn = await getUserJavascriptFunction({
|
|
20
20
|
context,
|
|
@@ -25,7 +25,8 @@ async function runTransformer({ context, input, refDef }) {
|
|
|
25
25
|
} catch (error) {
|
|
26
26
|
throw new ConfigError(`Error calling transformer "${refDef.transformer}" from "${refDef.path}".`, {
|
|
27
27
|
cause: error,
|
|
28
|
-
filePath:
|
|
28
|
+
filePath: referencedFrom,
|
|
29
|
+
lineNumber: refDef.lineNumber
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
}
|