@lowdefy/build 0.0.0-experimental-20260113102133 → 0.0.0-experimental-20260122074446

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 (57) hide show
  1. package/dist/build/addKeys.js +31 -18
  2. package/dist/build/buildApi/validateStepReferences.js +3 -4
  3. package/dist/build/buildAuth/buildAuth.js +2 -1
  4. package/dist/build/buildAuth/buildAuthPlugins.js +13 -13
  5. package/dist/build/buildAuth/validateAuthConfig.js +40 -8
  6. package/dist/build/buildAuth/validateMutualExclusivity.js +7 -7
  7. package/dist/build/buildConnections.js +20 -39
  8. package/dist/build/buildMenu.js +9 -14
  9. package/dist/build/buildPages/buildBlock/buildEvents.js +13 -13
  10. package/dist/build/buildPages/buildBlock/buildRequests.js +15 -15
  11. package/dist/build/buildPages/buildBlock/validateBlock.js +13 -13
  12. package/dist/build/buildPages/buildPage.js +5 -5
  13. package/dist/build/buildPages/validateLinkReferences.js +8 -9
  14. package/dist/build/buildPages/validatePayloadReferences.js +3 -4
  15. package/dist/build/buildPages/validateRequestReferences.js +7 -8
  16. package/dist/build/buildPages/validateStateReferences.js +14 -6
  17. package/dist/build/buildRefs/buildRefs.js +8 -0
  18. package/dist/build/buildRefs/evaluateBuildOperators.js +11 -1
  19. package/dist/build/buildRefs/evaluateStaticOperators.js +54 -0
  20. package/dist/build/buildRefs/getConfigFile.js +27 -6
  21. package/dist/build/buildRefs/getRefContent.js +15 -5
  22. package/dist/build/buildRefs/makeRefDefinition.js +1 -1
  23. package/dist/build/buildRefs/parseRefContent.js +32 -2
  24. package/dist/build/buildRefs/recursiveBuild.js +9 -7
  25. package/dist/build/buildRefs/runRefResolver.js +13 -2
  26. package/dist/build/buildTypes.js +9 -0
  27. package/dist/build/collectDynamicIdentifiers.js +35 -0
  28. package/dist/{utils/formatConfigError.js → build/collectTypeNames.js} +20 -8
  29. package/dist/build/formatBuildError.js +1 -1
  30. package/dist/build/testSchema.js +45 -6
  31. package/dist/build/validateOperatorsDynamic.js +28 -0
  32. package/dist/build/writeRequests.js +3 -3
  33. package/dist/createContext.js +42 -1
  34. package/dist/defaultTypesMap.js +480 -480
  35. package/dist/index.js +43 -4
  36. package/dist/lowdefySchema.js +60 -0
  37. package/dist/test-utils/parseTestYaml.js +110 -0
  38. package/dist/test-utils/runBuild.js +270 -0
  39. package/dist/test-utils/runBuildForSnapshots.js +698 -0
  40. package/dist/{test → test-utils}/testContext.js +15 -1
  41. package/dist/utils/collectConfigError.js +6 -6
  42. package/dist/utils/countOperators.js +5 -3
  43. package/dist/utils/createCheckDuplicateId.js +1 -1
  44. package/dist/utils/findConfigKey.js +37 -0
  45. package/dist/utils/makeId.js +12 -7
  46. package/dist/utils/tryBuildStep.js +12 -5
  47. package/package.json +39 -39
  48. package/dist/utils/formatConfigMessage.js +0 -33
  49. package/dist/utils/formatConfigWarning.js +0 -24
  50. package/dist/utils/formatErrorMessage.js +0 -56
  51. /package/dist/{test → test-utils}/buildRefs/testBuildRefsAsyncFunction.js +0 -0
  52. /package/dist/{test → test-utils}/buildRefs/testBuildRefsErrorResolver.js +0 -0
  53. /package/dist/{test → test-utils}/buildRefs/testBuildRefsNullResolver.js +0 -0
  54. /package/dist/{test → test-utils}/buildRefs/testBuildRefsParsingResolver.js +0 -0
  55. /package/dist/{test → test-utils}/buildRefs/testBuildRefsResolver.js +0 -0
  56. /package/dist/{test → test-utils}/buildRefs/testBuildRefsTransform.js +0 -0
  57. /package/dist/{test → test-utils}/buildRefs/testBuildRefsTransformIdentity.js +0 -0
@@ -13,26 +13,26 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/node-utils';
16
17
  import buildBlock from './buildBlock/buildBlock.js';
17
18
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
18
19
  import createCounter from '../../utils/createCounter.js';
19
- import formatConfigError from '../../utils/formatConfigError.js';
20
20
  import validateRequestReferences from './validateRequestReferences.js';
21
21
  function buildPage({ page, index, context, checkDuplicatePageId }) {
22
22
  const configKey = page['~k'];
23
23
  if (type.isUndefined(page.id)) {
24
- throw new Error(formatConfigError({
24
+ throw new ConfigError({
25
25
  message: `Page id missing at page ${index}.`,
26
26
  configKey,
27
27
  context
28
- }));
28
+ });
29
29
  }
30
30
  if (!type.isString(page.id)) {
31
- throw new Error(formatConfigError({
31
+ throw new ConfigError({
32
32
  message: `Page id is not a string at page ${index}. Received ${JSON.stringify(page.id)}.`,
33
33
  configKey,
34
34
  context
35
- }));
35
+ });
36
36
  }
37
37
  checkDuplicatePageId({
38
38
  id: page.id,
@@ -12,21 +12,20 @@
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 formatConfigError from '../../utils/formatConfigError.js';
16
- function validateLinkReferences({ linkActionRefs, pageIds, context }) {
15
+ */ function validateLinkReferences({ linkActionRefs, pageIds, context }) {
17
16
  const pageIdSet = new Set(pageIds);
18
17
  linkActionRefs.forEach(({ pageId, action, sourcePageId })=>{
18
+ // Only skip validation if skip is explicitly true
19
+ // Pages must exist in app even if Link is conditional
20
+ if (action.skip === true) {
21
+ return;
22
+ }
19
23
  if (!pageIdSet.has(pageId)) {
20
- const errorMessage = formatConfigError({
24
+ context.logger.configWarning({
21
25
  message: `Page "${pageId}" not found. Link on page "${sourcePageId}" references non-existent page.`,
22
26
  configKey: action['~k'],
23
- context
27
+ prodError: true
24
28
  });
25
- if (context.stage === 'dev' || context.stage === 'test') {
26
- context.logger.warn(errorMessage);
27
- } else {
28
- throw new Error(errorMessage);
29
- }
30
29
  }
31
30
  });
32
31
  }
@@ -13,7 +13,6 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import extractOperatorKey from '../../utils/extractOperatorKey.js';
16
- import formatConfigWarning from '../../utils/formatConfigWarning.js';
17
16
  import traverseConfig from '../../utils/traverseConfig.js';
18
17
  function validatePayloadReferences({ page, context }) {
19
18
  const requests = page.requests || [];
@@ -41,11 +40,11 @@ function validatePayloadReferences({ page, context }) {
41
40
  payloadRefs.forEach((configKey, topLevelKey)=>{
42
41
  if (payloadKeys.has(topLevelKey)) return;
43
42
  const message = `_payload references "${topLevelKey}" in request "${request.requestId}" on page "${page.pageId}", ` + `but no key "${topLevelKey}" exists in the request payload definition. ` + `Payload keys are defined in the request's "payload" property. ` + `Check for typos or add the key to the payload definition.`;
44
- context.logger.warn(formatConfigWarning({
43
+ context.logger.configWarning({
45
44
  message,
46
45
  configKey,
47
- context
48
- }));
46
+ prodError: true
47
+ });
49
48
  });
50
49
  });
51
50
  }
@@ -12,21 +12,20 @@
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 formatConfigError from '../../utils/formatConfigError.js';
15
+ */ import { type } from '@lowdefy/helpers';
16
16
  function validateRequestReferences({ requestActionRefs, requests, pageId, context }) {
17
17
  const requestIds = new Set(requests.map((req)=>req.requestId));
18
18
  requestActionRefs.forEach(({ requestId, action })=>{
19
+ // Skip validation if action has skip condition (true or operator object)
20
+ if (action.skip === true || type.isObject(action.skip)) {
21
+ return;
22
+ }
19
23
  if (!requestIds.has(requestId)) {
20
- const errorMessage = formatConfigError({
24
+ context.logger.configWarning({
21
25
  message: `Request "${requestId}" not defined on page "${pageId}".`,
22
26
  configKey: action['~k'],
23
- context
27
+ prodError: true
24
28
  });
25
- if (context.stage === 'dev' || context.stage === 'test') {
26
- context.logger.warn(errorMessage);
27
- } else {
28
- throw new Error(errorMessage);
29
- }
30
29
  }
31
30
  });
32
31
  }
@@ -13,12 +13,12 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import extractOperatorKey from '../../utils/extractOperatorKey.js';
16
- import formatConfigWarning from '../../utils/formatConfigWarning.js';
17
16
  import traverseConfig from '../../utils/traverseConfig.js';
18
17
  function validateStateReferences({ page, context }) {
19
- // Single traversal collects both blockIds and _state references
18
+ // Single traversal collects blockIds, _state references, and SetState keys
20
19
  // More memory-efficient than stringify+regex for massive pages
21
20
  const blockIds = new Set();
21
+ const setStateKeys = new Set();
22
22
  const stateRefs = new Map(); // topLevelKey -> configKey (first occurrence)
23
23
  traverseConfig({
24
24
  config: page,
@@ -27,6 +27,13 @@ function validateStateReferences({ page, context }) {
27
27
  if (obj.blockId) {
28
28
  blockIds.add(obj.blockId);
29
29
  }
30
+ // Collect SetState action params to track state keys being initialized
31
+ if (obj.type === 'SetState' && obj.params) {
32
+ Object.keys(obj.params).forEach((key)=>{
33
+ const topLevelKey = key.split(/[.\[]/)[0];
34
+ setStateKeys.add(topLevelKey);
35
+ });
36
+ }
30
37
  // Collect _state reference if present
31
38
  if (obj._state !== undefined) {
32
39
  const topLevelKey = extractOperatorKey({
@@ -40,13 +47,14 @@ function validateStateReferences({ page, context }) {
40
47
  });
41
48
  // Filter to only undefined references and warn
42
49
  stateRefs.forEach((configKey, topLevelKey)=>{
43
- if (blockIds.has(topLevelKey)) return;
50
+ // Skip if state key is from an input block or SetState action
51
+ if (blockIds.has(topLevelKey) || setStateKeys.has(topLevelKey)) return;
44
52
  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.`;
45
- context.logger.warn(formatConfigWarning({
53
+ context.logger.configWarning({
46
54
  message,
47
55
  configKey,
48
- context
49
- }));
56
+ prodError: true
57
+ });
50
58
  });
51
59
  }
52
60
  export default validateStateReferences;
@@ -15,6 +15,7 @@
15
15
  */ import recursiveBuild from './recursiveBuild.js';
16
16
  import makeRefDefinition from './makeRefDefinition.js';
17
17
  import evaluateBuildOperators from './evaluateBuildOperators.js';
18
+ import evaluateStaticOperators from './evaluateStaticOperators.js';
18
19
  async function buildRefs({ context }) {
19
20
  const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
20
21
  let components = await recursiveBuild({
@@ -22,11 +23,18 @@ async function buildRefs({ context }) {
22
23
  refDef,
23
24
  count: 0
24
25
  });
26
+ // First: evaluate _build.* operators (e.g., _build.env)
25
27
  components = await evaluateBuildOperators({
26
28
  context,
27
29
  input: components,
28
30
  refDef
29
31
  });
32
+ // Second: evaluate static operators (_sum, _if, etc.) that don't depend on runtime data
33
+ components = evaluateStaticOperators({
34
+ context,
35
+ input: components,
36
+ refDef
37
+ });
30
38
  return components ?? {};
31
39
  }
32
40
  export default buildRefs;
@@ -14,10 +14,20 @@
14
14
  limitations under the License.
15
15
  */ import { BuildParser } from '@lowdefy/operators';
16
16
  import operators from '@lowdefy/operators-js/operators/build';
17
+ import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
18
+ import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
19
+ // Validate and collect dynamic identifiers once at module load
20
+ validateOperatorsDynamic({
21
+ operators
22
+ });
23
+ const dynamicIdentifiers = collectDynamicIdentifiers({
24
+ operators
25
+ });
17
26
  async function evaluateBuildOperators({ context, input, refDef }) {
18
27
  const operatorsParser = new BuildParser({
19
28
  env: process.env,
20
- operators
29
+ operators,
30
+ dynamicIdentifiers
21
31
  });
22
32
  const { output, errors } = operatorsParser.parse({
23
33
  input,
@@ -0,0 +1,54 @@
1
+ /*
2
+ Copyright 2020-2024 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 { BuildParser } from '@lowdefy/operators';
16
+ import operators from '@lowdefy/operators-js/operators/build';
17
+ import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
18
+ import collectTypeNames from '../collectTypeNames.js';
19
+ import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
20
+ // Validate and collect dynamic identifiers once at module load
21
+ validateOperatorsDynamic({
22
+ operators
23
+ });
24
+ const dynamicIdentifiers = collectDynamicIdentifiers({
25
+ operators
26
+ });
27
+ function evaluateStaticOperators({ context, input, refDef }) {
28
+ // Collect type names from context.typesMap for type boundary detection
29
+ const typeNames = collectTypeNames({
30
+ typesMap: context.typesMap
31
+ });
32
+ const operatorsParser = new BuildParser({
33
+ env: process.env,
34
+ operators,
35
+ dynamicIdentifiers,
36
+ typeNames
37
+ });
38
+ const location = refDef.path ?? refDef.resolver;
39
+ const { output, errors } = operatorsParser.parse({
40
+ input,
41
+ location,
42
+ operatorPrefix: '_'
43
+ });
44
+ if (errors.length > 0) {
45
+ errors.forEach((error)=>{
46
+ context.logger.configWarning({
47
+ message: error.message,
48
+ operatorLocation: error.operatorLocation
49
+ });
50
+ });
51
+ }
52
+ return output;
53
+ }
54
+ export default evaluateStaticOperators;
@@ -12,17 +12,38 @@
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 path from 'path';
16
+ import { type } from '@lowdefy/helpers';
17
+ import { ConfigError } from '@lowdefy/node-utils';
16
18
  async function getConfigFile({ context, refDef, referencedFrom }) {
17
19
  if (!type.isString(refDef.path)) {
18
- throw new Error(`Invalid _ref definition ${JSON.stringify({
19
- _ref: refDef.original
20
- })} in file ${referencedFrom}`);
20
+ throw new ConfigError({
21
+ message: `Invalid _ref definition: ${JSON.stringify({
22
+ _ref: refDef.original
23
+ })}`,
24
+ filePath: referencedFrom,
25
+ lineNumber: refDef.lineNumber,
26
+ configDirectory: context.directories.config
27
+ });
21
28
  }
22
29
  const content = await context.readConfigFile(refDef.path);
23
30
  if (content === null) {
24
- const lineInfo = refDef.lineNumber ? `:${refDef.lineNumber}` : '';
25
- throw new Error(`Tried to reference file "${refDef.path}" from "${referencedFrom}${lineInfo}", but file does not exist.`);
31
+ const absolutePath = path.resolve(context.directories.config, refDef.path);
32
+ let message = `Referenced file does not exist: "${refDef.path}". Resolved to: ${absolutePath}`;
33
+ // Help with common mistakes
34
+ if (refDef.path.startsWith('../')) {
35
+ const suggestedPath = refDef.path.replace(/^(\.\.\/)+/, '');
36
+ message += ` Tip: Paths in _ref are resolved from config root. Did you mean "${suggestedPath}"?`;
37
+ } else if (refDef.path.startsWith('./')) {
38
+ const suggestedPath = refDef.path.substring(2);
39
+ message += ` Tip: Remove "./" prefix - paths are resolved from config root. Did you mean "${suggestedPath}"?`;
40
+ }
41
+ throw new ConfigError({
42
+ message,
43
+ filePath: referencedFrom,
44
+ lineNumber: refDef.lineNumber,
45
+ configDirectory: context.directories.config
46
+ });
26
47
  }
27
48
  return content;
28
49
  }
@@ -12,7 +12,8 @@
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 getConfigFile from './getConfigFile.js';
15
+ */ import { ConfigError } from '@lowdefy/node-utils';
16
+ import getConfigFile from './getConfigFile.js';
16
17
  import parseRefContent from './parseRefContent.js';
17
18
  import runRefResolver from './runRefResolver.js';
18
19
  async function getRefContent({ context, refDef, referencedFrom }) {
@@ -36,9 +37,18 @@ async function getRefContent({ context, refDef, referencedFrom }) {
36
37
  referencedFrom
37
38
  });
38
39
  }
39
- return parseRefContent({
40
- content,
41
- refDef
42
- });
40
+ try {
41
+ return parseRefContent({
42
+ content,
43
+ refDef
44
+ });
45
+ } catch (error) {
46
+ // Re-throw parse errors as ConfigError with location info
47
+ throw new ConfigError({
48
+ message: `Error parsing "${refDef.path}": ${error.message}`,
49
+ filePath: refDef.path,
50
+ configDirectory: context.directories.config
51
+ });
52
+ }
43
53
  }
44
54
  export default getRefContent;
@@ -16,7 +16,7 @@
16
16
  import getRefPath from './getRefPath.js';
17
17
  import makeId from '../../utils/makeId.js';
18
18
  function makeRefDefinition(refDefinition, parent, refMap, lineNumber) {
19
- const id = makeId();
19
+ const id = makeId.next();
20
20
  const refDef = {
21
21
  parent,
22
22
  lineNumber
@@ -36,10 +36,32 @@ function addLineNumbers(node, content, result) {
36
36
  if (isPair(pair) && isScalar(pair.key)) {
37
37
  const key = pair.key.value;
38
38
  const value = pair.value;
39
+ // Use key's line number for the value's ~l (more useful for error messages)
40
+ const keyLineNumber = pair.key.range ? getLineNumber(content, pair.key.range[0]) : null;
39
41
  if (isMap(value)) {
40
- obj[key] = addLineNumbers(value, content, {});
42
+ const mapResult = addLineNumbers(value, content, {});
43
+ // Override ~l with key's line number if available
44
+ if (keyLineNumber) {
45
+ Object.defineProperty(mapResult, '~l', {
46
+ value: keyLineNumber,
47
+ enumerable: false,
48
+ writable: true,
49
+ configurable: true
50
+ });
51
+ }
52
+ obj[key] = mapResult;
41
53
  } else if (isSeq(value)) {
42
- obj[key] = addLineNumbers(value, content, []);
54
+ const arrResult = addLineNumbers(value, content, []);
55
+ // Override ~l with key's line number if available
56
+ if (keyLineNumber) {
57
+ Object.defineProperty(arrResult, '~l', {
58
+ value: keyLineNumber,
59
+ enumerable: false,
60
+ writable: true,
61
+ configurable: true
62
+ });
63
+ }
64
+ obj[key] = arrResult;
43
65
  } else if (isScalar(value)) {
44
66
  obj[key] = value.value;
45
67
  } else {
@@ -51,6 +73,14 @@ function addLineNumbers(node, content, result) {
51
73
  }
52
74
  if (isSeq(node)) {
53
75
  const arr = result || [];
76
+ if (node.range) {
77
+ Object.defineProperty(arr, '~l', {
78
+ value: getLineNumber(content, node.range[0]),
79
+ enumerable: false,
80
+ writable: true,
81
+ configurable: true
82
+ });
83
+ }
54
84
  for (const item of node.items){
55
85
  if (isMap(item)) {
56
86
  arr.push(addLineNumbers(item, content, {}));
@@ -13,8 +13,8 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { serializer, type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/node-utils';
16
17
  import evaluateBuildOperators from './evaluateBuildOperators.js';
17
- import formatConfigError from '../../utils/formatConfigError.js';
18
18
  import getKey from './getKey.js';
19
19
  import getRefContent from './getRefContent.js';
20
20
  import getRefsFromFile from './getRefsFromFile.js';
@@ -30,20 +30,22 @@ async function recursiveBuild({ context, refDef, count, referencedFrom, refChain
30
30
  ...refChainList,
31
31
  currentPath
32
32
  ].join('\n -> ');
33
- throw new Error(formatConfigError({
34
- message: `Circular reference detected.\nFile "${currentPath}" references itself through:\n -> ${chainDisplay}`,
35
- context
36
- }));
33
+ throw new ConfigError({
34
+ message: `Circular reference detected. File "${currentPath}" references itself through:\n -> ${chainDisplay}`,
35
+ filePath: referencedFrom,
36
+ lineNumber: refDef.lineNumber,
37
+ configDirectory: context.directories.config
38
+ });
37
39
  }
38
40
  refChainSet.add(currentPath);
39
41
  refChainList.push(currentPath);
40
42
  }
41
43
  // Keep count as a fallback safety limit
42
44
  if (count > 10000) {
43
- throw new Error(formatConfigError({
45
+ throw new ConfigError({
44
46
  message: `Maximum recursion depth of references exceeded (10000 levels). This likely indicates a circular reference.`,
45
47
  context
46
- }));
48
+ });
47
49
  }
48
50
  let fileContent = await getRefContent({
49
51
  context,
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/node-utils';
16
17
  import getUserJavascriptFunction from './getUserJavascriptFunction.js';
17
18
  async function runRefResolver({ context, refDef, referencedFrom }) {
18
19
  const resolverFn = await getUserJavascriptFunction({
@@ -23,10 +24,20 @@ async function runRefResolver({ context, refDef, referencedFrom }) {
23
24
  try {
24
25
  content = await resolverFn(refDef.path, refDef.vars, context);
25
26
  } catch (error) {
26
- throw new Error(`Error calling resolver "${refDef.resolver}" from "${referencedFrom}": ${error.message}`);
27
+ throw new ConfigError({
28
+ message: `Error calling resolver "${refDef.resolver}": ${error.message}`,
29
+ filePath: referencedFrom,
30
+ lineNumber: refDef.lineNumber,
31
+ configDirectory: context.directories.config
32
+ });
27
33
  }
28
34
  if (type.isNone(content)) {
29
- throw new Error(`Tried to reference with resolver "${refDef.resolver}" from "${referencedFrom}", but received "${content}".`);
35
+ throw new ConfigError({
36
+ message: `Resolver "${refDef.resolver}" returned "${content}".`,
37
+ filePath: referencedFrom,
38
+ lineNumber: refDef.lineNumber,
39
+ configDirectory: context.directories.config
40
+ });
30
41
  }
31
42
  return content;
32
43
  }
@@ -16,11 +16,20 @@
16
16
  import loaderTypes from '@lowdefy/blocks-loaders/types';
17
17
  import findSimilarString from '../utils/findSimilarString.js';
18
18
  import formatBuildError from './formatBuildError.js';
19
+ // Check if a configKey has ~ignoreBuildCheck set
20
+ function hasIgnoreBuildCheck(keyMap, configKey) {
21
+ return keyMap[configKey]?.['~ignoreBuildCheck'] === true;
22
+ }
19
23
  function buildTypeClass(context, { counter, definitions, store, typeClass, warnIfMissing = false }) {
20
24
  const counts = counter.getCounts();
21
25
  const definedTypes = Object.keys(definitions);
22
26
  Object.keys(counts).forEach((typeName)=>{
23
27
  if (!definitions[typeName]) {
28
+ // Check if this type usage has ~ignoreBuildCheck flag
29
+ const configKey = counter.getLocation(typeName);
30
+ if (configKey && hasIgnoreBuildCheck(context.keyMap, configKey)) {
31
+ return; // Skip warning/error for this type
32
+ }
24
33
  let message = `${typeClass} type "${typeName}" was used but is not defined.`;
25
34
  const suggestion = findSimilarString({
26
35
  input: typeName,
@@ -0,0 +1,35 @@
1
+ /*
2
+ Copyright 2020-2024 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
+ function collectDynamicIdentifiers({ operators }) {
17
+ const dynamicIdentifiers = new Set();
18
+ Object.entries(operators).forEach(([operatorName, operatorFn])=>{
19
+ if (!type.isFunction(operatorFn)) return;
20
+ if (operatorFn.dynamic === true) {
21
+ dynamicIdentifiers.add(operatorName);
22
+ return;
23
+ }
24
+ // Check for method-level dynamic in meta
25
+ if (type.isObject(operatorFn.meta)) {
26
+ Object.entries(operatorFn.meta).forEach(([methodName, methodMeta])=>{
27
+ if (type.isObject(methodMeta) && methodMeta.dynamic === true) {
28
+ dynamicIdentifiers.add(`${operatorName}.${methodName}`);
29
+ }
30
+ });
31
+ }
32
+ });
33
+ return dynamicIdentifiers;
34
+ }
35
+ export default collectDynamicIdentifiers;
@@ -12,13 +12,25 @@
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 formatConfigMessage from './formatConfigMessage.js';
16
- function formatConfigError({ message, configKey, context }) {
17
- return formatConfigMessage({
18
- prefix: '[Config Error]',
19
- message,
20
- configKey,
21
- context
15
+ */ import { type } from '@lowdefy/helpers';
16
+ function collectTypeNames({ typesMap }) {
17
+ const typeNames = new Set();
18
+ if (!type.isObject(typesMap)) {
19
+ return typeNames;
20
+ }
21
+ [
22
+ 'blocks',
23
+ 'requests',
24
+ 'connections',
25
+ 'actions',
26
+ 'controls'
27
+ ].forEach((category)=>{
28
+ if (type.isObject(typesMap[category])) {
29
+ Object.keys(typesMap[category]).forEach((typeName)=>{
30
+ typeNames.add(typeName);
31
+ });
32
+ }
22
33
  });
34
+ return typeNames;
23
35
  }
24
- export default formatConfigError;
36
+ export default collectTypeNames;
@@ -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 { resolveConfigLocation } from '@lowdefy/helpers';
15
+ */ import { resolveConfigLocation } from '@lowdefy/node-utils';
16
16
  function formatBuildError({ context, counter, typeName, message }) {
17
17
  const configKey = counter.getLocation(typeName);
18
18
  if (!configKey) {
@@ -13,8 +13,9 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { validate } from '@lowdefy/ajv';
16
+ import { ConfigError } from '@lowdefy/node-utils';
17
+ import findConfigKey from '../utils/findConfigKey.js';
16
18
  import lowdefySchema from '../lowdefySchema.js';
17
- import formatErrorMessage from '../utils/formatErrorMessage.js';
18
19
  function testSchema({ components, context }) {
19
20
  const { valid, errors } = validate({
20
21
  schema: lowdefySchema,
@@ -22,11 +23,49 @@ function testSchema({ components, context }) {
22
23
  returnErrors: true
23
24
  });
24
25
  if (!valid) {
25
- context.logger.warn('Schema not valid.');
26
- errors.map((error)=>context.logger.warn(formatErrorMessage({
27
- error,
28
- components
29
- })));
26
+ // Filter out anyOf/oneOf cascade errors - these are always accompanied by
27
+ // more specific validation errors and just add noise
28
+ let filteredErrors = errors.filter((error)=>error.keyword !== 'anyOf' && error.keyword !== 'oneOf');
29
+ // Hierarchical deduplication: if an error exists at a child path,
30
+ // filter out errors at parent paths (prefer more specific errors)
31
+ filteredErrors = filteredErrors.filter((error)=>{
32
+ const hasChildError = filteredErrors.some((other)=>other !== error && other.instancePath.startsWith(error.instancePath + '/'));
33
+ return !hasChildError;
34
+ });
35
+ // Same-path deduplication: only show first error per unique path
36
+ // (multiple errors at same path are usually cascade errors from schema branches)
37
+ const seenPaths = new Set();
38
+ filteredErrors = filteredErrors.filter((error)=>{
39
+ if (seenPaths.has(error.instancePath)) {
40
+ return false;
41
+ }
42
+ seenPaths.add(error.instancePath);
43
+ return true;
44
+ });
45
+ filteredErrors.forEach((error)=>{
46
+ const instancePath = error.instancePath.split('/').slice(1).filter(Boolean);
47
+ const configKey = findConfigKey({
48
+ components,
49
+ instancePath
50
+ });
51
+ let message = error.message;
52
+ if (error.params?.additionalProperty) {
53
+ message = `${message} - "${error.params.additionalProperty}"`;
54
+ }
55
+ const configError = new ConfigError({
56
+ message,
57
+ configKey,
58
+ context
59
+ });
60
+ if (!configError.suppressed) {
61
+ if (!context.errors) {
62
+ // If no error collection array, throw immediately (fallback for tests)
63
+ throw new Error(configError.message);
64
+ }
65
+ // Collect error - logging happens at checkpoints in index.js
66
+ context.errors.push(configError.message);
67
+ }
68
+ });
30
69
  }
31
70
  }
32
71
  export default testSchema;