@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.
Files changed (38) hide show
  1. package/dist/build/buildAgents.js +249 -0
  2. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  3. package/dist/build/buildApi/buildRoutine/setStepId.js +6 -2
  4. package/dist/build/buildApi/buildRoutine/validateStep.js +18 -0
  5. package/dist/build/buildApp.js +18 -5
  6. package/dist/build/buildImports/buildImportsDev.js +5 -0
  7. package/dist/build/buildImports/buildImportsProd.js +1 -0
  8. package/dist/build/buildJs/writeJs.js +2 -2
  9. package/dist/build/buildMenu.js +5 -0
  10. package/dist/build/buildModuleDefs.js +52 -1
  11. package/dist/build/buildModules.js +1 -1
  12. package/dist/build/buildPages/validateCallApiRefs.js +9 -0
  13. package/dist/build/buildRefs/getUserJavascriptFunction.js +6 -9
  14. package/dist/build/buildRefs/runTransformer.js +3 -2
  15. package/dist/build/buildRefs/walker.js +22 -39
  16. package/dist/build/buildTypes.js +7 -0
  17. package/dist/build/codegenI18nLocales.js +53 -0
  18. package/dist/build/copyAgentFileSystems.js +45 -0
  19. package/dist/build/full/updateServerPackageJson.js +1 -0
  20. package/dist/build/jit/shallowBuild.js +31 -0
  21. package/dist/build/registerModules.js +11 -33
  22. package/dist/build/writeAgents.js +26 -0
  23. package/dist/build/writeAppMeta.js +19 -0
  24. package/dist/build/writeI18n.js +117 -0
  25. package/dist/build/writePluginImports/writeAgentImports.js +22 -0
  26. package/dist/build/writePluginImports/writePluginImports.js +10 -0
  27. package/dist/build/writePluginImports/writeServerExternalPackages.js +121 -0
  28. package/dist/createContext.js +8 -1
  29. package/dist/defaultMessagesMap.js +3 -0
  30. package/dist/defaultPackages.js +7 -0
  31. package/dist/defaultTypesMap.js +590 -377
  32. package/dist/index.js +30 -0
  33. package/dist/lowdefySchema.js +414 -0
  34. package/dist/scripts/generateDefaultMessages.js +37 -0
  35. package/dist/scripts/generateDefaultTypes.js +1 -0
  36. package/dist/test-utils/testContext.js +2 -0
  37. package/dist/utils/createPluginTypesMap.js +7 -0
  38. 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
- */ function setStepId(step, { endpointId }) {
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 === 'CallApi' ? 'endpoint' : 'request';
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
@@ -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
- try {
34
- components.app.git_sha = execSync('git rev-parse HEAD').toString().trim();
35
- } catch (_) {
36
- //pass
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, secrets, state, step, user }`
23
+ functionPrototype: `{ args, item, lowdefyApp, payload, secret, state, step, user }`
24
24
  }));
25
25
  }
26
26
  export default writeJs;
@@ -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 does not export a connection with that id.`);
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.join(context.directories.config, filePath));
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
- return (await nativeImport(fileUrl.href)).default;
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: refDef.transformer
28
+ filePath: referencedFrom,
29
+ lineNumber: refDef.lineNumber
29
30
  });
30
31
  }
31
32
  }