@lowdefy/build 4.5.2 → 4.6.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 (165) hide show
  1. package/dist/build/addDefaultPages/404.js +1 -1
  2. package/dist/build/addDefaultPages/addDefaultPages.js +10 -4
  3. package/dist/build/addKeys.js +85 -29
  4. package/dist/build/buildApi/buildApi.js +27 -8
  5. package/dist/build/buildApi/buildEndpoint.js +7 -1
  6. package/dist/build/buildApi/buildRoutine/buildControl.js +1 -1
  7. package/dist/build/buildApi/buildRoutine/buildRoutine.js +1 -1
  8. package/dist/build/buildApi/buildRoutine/buildStep.js +1 -1
  9. package/dist/build/buildApi/buildRoutine/controlTypes.js +34 -11
  10. package/dist/build/buildApi/buildRoutine/countControl.js +1 -1
  11. package/dist/build/buildApi/buildRoutine/countStepTypes.js +2 -2
  12. package/dist/build/buildApi/buildRoutine/setStepId.js +1 -1
  13. package/dist/build/buildApi/buildRoutine/validateStep.js +30 -9
  14. package/dist/build/buildApi/validateEndpoint.js +36 -7
  15. package/dist/build/buildApi/validateStepReferences.js +65 -0
  16. package/dist/build/buildApp.js +1 -1
  17. package/dist/build/buildAuth/buildApiAuth.js +7 -3
  18. package/dist/build/buildAuth/buildAuth.js +7 -4
  19. package/dist/build/buildAuth/buildAuthPlugins.js +42 -13
  20. package/dist/build/buildAuth/buildPageAuth.js +14 -3
  21. package/dist/build/buildAuth/getApiRoles.js +1 -1
  22. package/dist/build/buildAuth/getPageRoles.js +1 -1
  23. package/dist/build/buildAuth/getProtectedApi.js +1 -1
  24. package/dist/build/buildAuth/getProtectedPages.js +1 -1
  25. package/dist/build/buildAuth/validateAuthConfig.js +39 -5
  26. package/dist/build/buildAuth/validateMutualExclusivity.js +13 -5
  27. package/dist/build/buildConnections.js +23 -24
  28. package/dist/build/buildImports/buildIconImports.js +1 -1
  29. package/dist/build/buildImports/buildImports.js +1 -1
  30. package/dist/build/buildImports/buildImportsDev.js +1 -1
  31. package/dist/build/buildImports/buildImportsProd.js +1 -1
  32. package/dist/build/buildImports/buildStyleImports.js +1 -1
  33. package/dist/build/buildImports/defaultIconsDev.js +1 -1
  34. package/dist/build/buildImports/defaultIconsProd.js +1 -1
  35. package/dist/build/buildJs/generateJsFile.js +1 -1
  36. package/dist/build/buildJs/jsMapParser.js +1 -1
  37. package/dist/build/buildJs/writeJs.js +1 -1
  38. package/dist/build/buildLogger.js +41 -0
  39. package/dist/build/buildMenu.js +36 -12
  40. package/dist/build/buildPages/buildBlock/buildBlock.js +1 -1
  41. package/dist/build/buildPages/buildBlock/buildEvents.js +79 -9
  42. package/dist/build/buildPages/buildBlock/buildRequests.js +38 -8
  43. package/dist/build/buildPages/buildBlock/buildSubBlocks.js +6 -2
  44. package/dist/build/buildPages/buildBlock/countBlockOperators.js +1 -1
  45. package/dist/build/buildPages/buildBlock/countBlockTypes.js +2 -2
  46. package/dist/build/buildPages/buildBlock/moveSkeletonBlocksToArea.js +6 -2
  47. package/dist/build/buildPages/buildBlock/moveSubBlocksToArea.js +6 -2
  48. package/dist/build/buildPages/buildBlock/setBlockId.js +1 -1
  49. package/dist/build/buildPages/buildBlock/validateBlock.js +25 -7
  50. package/dist/build/buildPages/buildPage.js +35 -6
  51. package/dist/build/buildPages/validateLinkReferences.js +33 -0
  52. package/dist/build/buildPages/validatePayloadReferences.js +59 -0
  53. package/dist/build/buildPages/validateRequestReferences.js +33 -0
  54. package/dist/build/buildPages/validateServerStateReferences.js +41 -0
  55. package/dist/build/buildPages/validateStateReferences.js +79 -0
  56. package/dist/build/buildRefs/buildRefs.js +20 -3
  57. package/dist/build/buildRefs/createRefReviver.js +28 -0
  58. package/dist/build/buildRefs/evaluateBuildOperators.js +26 -7
  59. package/dist/build/buildRefs/evaluateStaticOperators.js +56 -0
  60. package/dist/build/buildRefs/getConfigFile.js +25 -6
  61. package/dist/build/buildRefs/getKey.js +1 -1
  62. package/dist/build/buildRefs/getRefContent.js +1 -1
  63. package/dist/build/buildRefs/getRefPath.js +1 -1
  64. package/dist/build/buildRefs/getRefsFromFile.js +9 -4
  65. package/dist/build/buildRefs/getUserJavascriptFunction.js +18 -4
  66. package/dist/build/buildRefs/makeRefDefinition.js +10 -5
  67. package/dist/build/buildRefs/parseNunjucks.js +1 -1
  68. package/dist/build/buildRefs/parseRefContent.js +100 -6
  69. package/dist/build/buildRefs/populateRefs.js +75 -13
  70. package/dist/build/buildRefs/recursiveBuild.js +66 -18
  71. package/dist/build/buildRefs/runRefResolver.js +11 -3
  72. package/dist/build/buildRefs/runTransformer.js +7 -3
  73. package/dist/build/buildTypes.js +22 -7
  74. package/dist/build/cleanBuildDirectory.js +1 -1
  75. package/dist/build/collectDynamicIdentifiers.js +35 -0
  76. package/dist/build/collectTypeNames.js +36 -0
  77. package/dist/build/copyPublicFolder.js +1 -1
  78. package/dist/build/{buildJs → full}/buildJs.js +3 -3
  79. package/dist/build/full/buildPages.js +87 -0
  80. package/dist/build/{buildPages → full}/buildTestPage.js +15 -3
  81. package/dist/build/{updateServerPackageJson.js → full/updateServerPackageJson.js} +1 -1
  82. package/dist/build/{writePages.js → full/writePages.js} +1 -1
  83. package/dist/build/{writeRequests.js → full/writeRequests.js} +11 -3
  84. package/dist/build/{writeTypes.js → full/writeTypes.js} +1 -1
  85. package/dist/build/jit/addInstalledTypes.js +51 -0
  86. package/dist/build/jit/buildJsShallow.js +41 -0
  87. package/dist/build/jit/buildPageJit.js +252 -0
  88. package/dist/build/jit/buildShallowPages.js +90 -0
  89. package/dist/build/jit/createPageRegistry.js +80 -0
  90. package/dist/build/jit/detectMissingPluginPackages.js +62 -0
  91. package/dist/build/jit/getRefPositions.js +38 -0
  92. package/dist/build/jit/isPageContentPath.js +24 -0
  93. package/dist/build/jit/pageContentKeys.js +26 -0
  94. package/dist/build/jit/shallowBuild.js +245 -0
  95. package/dist/build/jit/stripPageContent.js +23 -0
  96. package/dist/build/jit/updateServerPackageJsonJit.js +33 -0
  97. package/dist/build/jit/validatePageTypes.js +73 -0
  98. package/dist/build/jit/writePageJit.js +44 -0
  99. package/dist/build/jit/writePageRegistry.js +23 -0
  100. package/dist/build/jit/writeSourcelessPages.js +23 -0
  101. package/dist/build/testSchema.js +45 -7
  102. package/dist/build/validateConfig.js +2 -2
  103. package/dist/build/validateOperatorsDynamic.js +28 -0
  104. package/dist/build/writeApi.js +1 -1
  105. package/dist/build/writeApp.js +1 -1
  106. package/dist/build/writeAuth.js +1 -1
  107. package/dist/build/writeConfig.js +1 -1
  108. package/dist/build/writeConnections.js +1 -1
  109. package/dist/build/writeGlobal.js +1 -1
  110. package/dist/build/writeLogger.js +19 -0
  111. package/dist/build/writeMaps.js +1 -1
  112. package/dist/build/writeMenus.js +1 -1
  113. package/dist/build/writePluginImports/generateImportFile.js +1 -1
  114. package/dist/build/writePluginImports/writeActionImports.js +1 -1
  115. package/dist/build/writePluginImports/writeActionSchemaMap.js +42 -0
  116. package/dist/build/writePluginImports/writeAuthImports.js +1 -1
  117. package/dist/build/writePluginImports/writeBlockImports.js +1 -1
  118. package/dist/build/writePluginImports/writeBlockSchemaMap.js +42 -0
  119. package/dist/build/writePluginImports/writeConnectionImports.js +1 -1
  120. package/dist/build/writePluginImports/writeIconImports.js +1 -1
  121. package/dist/build/writePluginImports/writeOperatorImports.js +1 -1
  122. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +49 -0
  123. package/dist/build/writePluginImports/writePluginImports.js +16 -1
  124. package/dist/build/writePluginImports/writeStyleImports.js +1 -1
  125. package/dist/createContext.js +16 -4
  126. package/dist/defaultTypesMap.js +479 -457
  127. package/dist/index.js +188 -127
  128. package/dist/indexDev.js +18 -0
  129. package/dist/lowdefySchema.js +589 -0
  130. package/dist/scripts/generateDefaultTypes.js +2 -1
  131. package/dist/scripts/run.js +1 -1
  132. package/dist/{test → test-utils}/buildRefs/testBuildRefsAsyncFunction.js +1 -1
  133. package/dist/{test → test-utils}/buildRefs/testBuildRefsErrorResolver.js +1 -1
  134. package/dist/{test → test-utils}/buildRefs/testBuildRefsNullResolver.js +1 -1
  135. package/dist/{test → test-utils}/buildRefs/testBuildRefsParsingResolver.js +1 -1
  136. package/dist/{test → test-utils}/buildRefs/testBuildRefsResolver.js +1 -1
  137. package/dist/{test → test-utils}/buildRefs/testBuildRefsTransform.js +1 -1
  138. package/dist/{test → test-utils}/buildRefs/testBuildRefsTransformIdentity.js +1 -1
  139. package/dist/test-utils/buildRefs/testJitPageResolver.js +24 -0
  140. package/dist/test-utils/createTestLogger.js +36 -0
  141. package/dist/test-utils/parseTestYaml.js +126 -0
  142. package/dist/test-utils/runBuild.js +228 -0
  143. package/dist/test-utils/runBuildForSnapshots.js +704 -0
  144. package/dist/{test → test-utils}/testContext.js +12 -2
  145. package/dist/utils/collectExceptions.js +34 -0
  146. package/dist/utils/countOperators.js +31 -12
  147. package/dist/utils/createBuildHandleError.js +38 -0
  148. package/dist/utils/createCheckDuplicateId.js +15 -9
  149. package/dist/utils/createCounter.js +16 -3
  150. package/dist/utils/createHandleWarning.js +41 -0
  151. package/dist/utils/createPluginTypesMap.js +1 -1
  152. package/dist/utils/extractOperatorKey.js +36 -0
  153. package/dist/utils/findConfigKey.js +37 -0
  154. package/dist/utils/findSimilarString.js +53 -0
  155. package/dist/utils/logCollectedErrors.js +30 -0
  156. package/dist/utils/makeId.js +13 -8
  157. package/dist/utils/preserveMetaProperties.js +27 -0
  158. package/dist/utils/readConfigFile.js +1 -1
  159. package/dist/utils/setNonEnumerableProperty.js +23 -0
  160. package/dist/utils/traverseConfig.js +43 -0
  161. package/dist/utils/tryBuildStep.js +46 -0
  162. package/dist/utils/writeBuildArtifact.js +1 -1
  163. package/package.json +46 -41
  164. package/dist/build/buildPages/buildPages.js +0 -31
  165. package/dist/utils/formatErrorMessage.js +0 -56
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -12,32 +12,94 @@
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 { get, type } from '@lowdefy/helpers';
15
+ */ import { get, serializer, type } from '@lowdefy/helpers';
16
+ /**
17
+ * Copies a _var value while preserving source location markers.
18
+ *
19
+ * When _var copies a value from vars into a template, we need to preserve
20
+ * the ~r (ref ID) and ~l (line number) from WHERE THE VALUE IS DEFINED
21
+ * (the source file), not where _var is used (the template).
22
+ *
23
+ * The issue is that serializer.copy loses non-enumerable properties, and
24
+ * recursiveBuild.js then sets ~r to the template file for objects without ~r.
25
+ *
26
+ * This function preserves the source location by making ~r enumerable on
27
+ * the copied value, so it survives through subsequent serializer.copy calls.
28
+ *
29
+ * @param {*} value - The value to copy from vars
30
+ * @param {string} sourceRefId - The ref ID of the source file where the var is defined
31
+ * @returns {*} The copied value with preserved source location markers
32
+ */ function copyVarValue(value, sourceRefId) {
33
+ if (!type.isObject(value) && !type.isArray(value)) {
34
+ return value;
35
+ }
36
+ // Copy the value, preserving ~l and setting ~r to the source file
37
+ return serializer.copy(value, {
38
+ reviver: (_, v)=>{
39
+ if (type.isObject(v) || type.isArray(v)) {
40
+ // Preserve the source file's ref ID by setting it explicitly
41
+ // This prevents recursiveBuild from overwriting it with the template's ref ID
42
+ if (sourceRefId && v['~r'] === undefined) {
43
+ v['~r'] = sourceRefId;
44
+ }
45
+ }
46
+ return v;
47
+ }
48
+ });
49
+ }
16
50
  function refReviver(key, value) {
17
51
  if (type.isObject(value)) {
18
52
  if (!type.isUndefined(value._ref)) {
19
- return this.parsedFiles[value._ref.id];
53
+ const result = this.parsedFiles[value._ref.id];
54
+ if (value._ref.ignoreBuildChecks !== undefined) {
55
+ if (type.isObject(result)) {
56
+ result['~ignoreBuildChecks'] = value._ref.ignoreBuildChecks;
57
+ } else if (type.isArray(result)) {
58
+ result.forEach((item)=>{
59
+ if (type.isObject(item)) {
60
+ item['~ignoreBuildChecks'] = value._ref.ignoreBuildChecks;
61
+ }
62
+ });
63
+ }
64
+ }
65
+ return result;
20
66
  }
21
67
  if (value._var) {
22
68
  if (type.isString(value._var)) {
23
- return JSON.parse(JSON.stringify(get(this.vars, value._var, {
69
+ const varValue = get(this.vars, value._var, {
24
70
  default: null
25
- })));
71
+ });
72
+ return copyVarValue(varValue, this.sourceRefId);
26
73
  }
27
74
  if (type.isObject(value._var) && type.isString(value._var.key)) {
28
- return JSON.parse(JSON.stringify(get(this.vars, value._var.key, {
29
- default: type.isNone(value._var.default) ? null : value._var.default
30
- })));
75
+ const varKey = value._var.key;
76
+ const varFromParent = get(this.vars, varKey);
77
+ // Check if var was explicitly provided (even if null) vs not provided at all (undefined)
78
+ // - Var provided (including null): use parent's sourceRefId for location
79
+ // - Var not provided (undefined): use default, preserve template's location
80
+ if (!type.isUndefined(varFromParent)) {
81
+ // Var was explicitly provided from parent file - use parent's location
82
+ return copyVarValue(varFromParent, this.sourceRefId);
83
+ }
84
+ // Using default value defined in template - preserve template's location markers
85
+ // Pass null for sourceRefId so we don't override the template file's ~r
86
+ const defaultValue = type.isNone(value._var.default) ? null : value._var.default;
87
+ return copyVarValue(defaultValue, null);
31
88
  }
32
- throw new Error(`"_var" operator takes a string or object with "key" field as arguments. Received "${JSON.stringify(value)}"`);
89
+ throw new Error('_var operator takes a string or object with "key" field as arguments.');
33
90
  }
34
91
  }
35
92
  return value;
36
93
  }
37
94
  function populateRefs({ parsedFiles, refDef, toPopulate }) {
38
- return JSON.parse(JSON.stringify(toPopulate), refReviver.bind({
39
- parsedFiles,
40
- vars: refDef.vars
41
- }));
95
+ // Use serializer.copy to preserve non-enumerable properties like ~r, ~k, ~l
96
+ // sourceRefId is the PARENT file's ref ID where vars are defined (not the template's ID)
97
+ return serializer.copy(toPopulate, {
98
+ reviver: refReviver.bind({
99
+ parsedFiles,
100
+ vars: refDef.vars,
101
+ sourceRefId: refDef.parent
102
+ })
103
+ });
42
104
  }
43
105
  export default populateRefs;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -12,30 +12,65 @@
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 { serializer, type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/errors';
17
+ import createRefReviver from './createRefReviver.js';
16
18
  import evaluateBuildOperators from './evaluateBuildOperators.js';
17
19
  import getKey from './getKey.js';
18
20
  import getRefContent from './getRefContent.js';
21
+ import getRefPositions from '../jit/getRefPositions.js';
19
22
  import getRefsFromFile from './getRefsFromFile.js';
20
23
  import populateRefs from './populateRefs.js';
21
24
  import runTransformer from './runTransformer.js';
22
- async function recursiveBuild({ context, refDef, count, referencedFrom }) {
23
- // TODO: Maybe it would be better to detect a cycle, since this is the real issue here?
25
+ import isPageContentPath from '../jit/isPageContentPath.js';
26
+ async function recursiveBuild({ context, refDef, count, content, referencedFrom, refChainSet = new Set(), refChainList = [], shallowOptions, jsonPath = '' }) {
27
+ // Detect circular references by tracking the chain of files being resolved
28
+ // Skip circular reference checking for refs without paths (e.g., resolver refs)
29
+ const currentPath = refDef.path;
30
+ if (currentPath) {
31
+ if (refChainSet.has(currentPath)) {
32
+ const chainDisplay = [
33
+ ...refChainList,
34
+ currentPath
35
+ ].join('\n -> ');
36
+ throw new ConfigError(`Circular reference detected. File "${currentPath}" references itself through:\n -> ${chainDisplay}`, {
37
+ filePath: referencedFrom ?? null,
38
+ lineNumber: referencedFrom ? refDef.lineNumber : null
39
+ });
40
+ }
41
+ refChainSet.add(currentPath);
42
+ refChainList.push(currentPath);
43
+ }
44
+ // Keep count as a fallback safety limit
24
45
  if (count > 10000) {
25
- throw new Error(`Maximum recursion depth of references exceeded.`);
46
+ throw new ConfigError('Maximum recursion depth of references exceeded (10000 levels). This likely indicates a circular reference.');
26
47
  }
27
- let fileContent = await getRefContent({
48
+ let fileContent = content ?? await getRefContent({
28
49
  context,
29
50
  refDef,
30
51
  referencedFrom
31
52
  });
32
53
  const { foundRefs, fileContentBuiltRefs } = getRefsFromFile(fileContent, refDef.id, context.refMap);
33
54
  const parsedFiles = {};
55
+ // Compute the JSON path position of each ref within the current file's content.
56
+ // This lets us check if a ref should be skipped during shallow builds.
57
+ const refPositions = shallowOptions ? getRefPositions(fileContentBuiltRefs, jsonPath) : null;
34
58
  // Since we can have references in the variables of a reference, we need to first parse
35
59
  // the deeper nodes, so we can use those parsed files in references higher in the tree.
36
60
  // To do this, since foundRefs is an array of ref definitions that are in order of the
37
61
  // deepest nodes first we for loop over over foundRefs one by one, awaiting each result.
38
62
  for (const newRefDef of foundRefs.values()){
63
+ // Check if this ref should be skipped (shallow build)
64
+ const refJsonPath = refPositions?.get(newRefDef.id) ?? '';
65
+ if (shallowOptions && isPageContentPath(refJsonPath)) {
66
+ // Store shallow marker instead of resolving
67
+ parsedFiles[newRefDef.id] = {
68
+ '~shallow': true,
69
+ _ref: newRefDef.original,
70
+ _refId: newRefDef.id
71
+ };
72
+ continue;
73
+ }
39
74
  // Parse vars and path before passing down to parse new file
40
75
  const parsedRefDef = populateRefs({
41
76
  toPopulate: newRefDef,
@@ -43,11 +78,22 @@ async function recursiveBuild({ context, refDef, count, referencedFrom }) {
43
78
  refDef
44
79
  });
45
80
  context.refMap[parsedRefDef.id].path = parsedRefDef.path;
81
+ // Store original definition for resolver refs so JIT can re-run them
82
+ if (!parsedRefDef.path) {
83
+ context.refMap[parsedRefDef.id].original = newRefDef.original;
84
+ }
85
+ if (Object.keys(newRefDef.vars).length > 0) {
86
+ context.unresolvedRefVars[newRefDef.id] = newRefDef.vars;
87
+ }
46
88
  const parsedFile = await recursiveBuild({
47
89
  context,
48
90
  refDef: parsedRefDef,
49
91
  count: count + 1,
50
- referencedFrom: refDef.path
92
+ referencedFrom: refDef.path ?? referencedFrom,
93
+ refChainSet,
94
+ refChainList,
95
+ shallowOptions,
96
+ jsonPath: refJsonPath
51
97
  });
52
98
  const transformedFile = await runTransformer({
53
99
  context,
@@ -64,17 +110,19 @@ async function recursiveBuild({ context, refDef, count, referencedFrom }) {
64
110
  input: evaluatedOperators,
65
111
  refDef: parsedRefDef
66
112
  });
67
- const reviver = (_, value)=>{
68
- if (!type.isObject(value)) return value;
69
- Object.defineProperty(value, '~r', {
70
- value: refDef.id,
71
- enumerable: false,
72
- writable: true,
73
- configurable: true
74
- });
75
- return value;
76
- };
77
- parsedFiles[newRefDef.id] = JSON.parse(JSON.stringify(withRefKey), reviver);
113
+ // Use serializer.copy to preserve non-enumerable properties like ~l.
114
+ // Only set ~r if not already present to preserve original file references from nested imports.
115
+ // Use child file's ref ID (parsedRefDef.id) not parent's (refDef.id) for correct error tracing.
116
+ const reviver = createRefReviver(parsedRefDef.id);
117
+ parsedFiles[newRefDef.id] = serializer.copy(withRefKey, {
118
+ reviver
119
+ });
120
+ }
121
+ // Backtrack: remove current file from chain so sibling refs can use it
122
+ // Only remove if it was added (i.e., if currentPath exists)
123
+ if (currentPath) {
124
+ refChainSet.delete(currentPath);
125
+ refChainList.pop();
78
126
  }
79
127
  return populateRefs({
80
128
  toPopulate: fileContentBuiltRefs,
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -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/errors';
16
17
  import getUserJavascriptFunction from './getUserJavascriptFunction.js';
17
18
  async function runRefResolver({ context, refDef, referencedFrom }) {
18
19
  const resolverFn = await getUserJavascriptFunction({
@@ -23,10 +24,17 @@ 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(`Error calling resolver "${refDef.resolver}".`, {
28
+ cause: error,
29
+ filePath: referencedFrom,
30
+ lineNumber: refDef.lineNumber
31
+ });
27
32
  }
28
33
  if (type.isNone(content)) {
29
- throw new Error(`Tried to reference with resolver "${refDef.resolver}" from "${referencedFrom}", but received "${content}".`);
34
+ throw new ConfigError(`Resolver "${refDef.resolver}" returned "${content}".`, {
35
+ filePath: referencedFrom,
36
+ lineNumber: refDef.lineNumber
37
+ });
30
38
  }
31
39
  return content;
32
40
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -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 getUserJavascriptFunction from './getUserJavascriptFunction.js';
15
+ */ import { ConfigError } from '@lowdefy/errors';
16
+ import getUserJavascriptFunction from './getUserJavascriptFunction.js';
16
17
  async function runTransformer({ context, input, refDef }) {
17
18
  if (refDef.transformer) {
18
19
  const transformerFn = await getUserJavascriptFunction({
@@ -22,7 +23,10 @@ async function runTransformer({ context, input, refDef }) {
22
23
  try {
23
24
  return transformerFn(input, refDef.vars);
24
25
  } catch (error) {
25
- throw Error(`Error calling transformer "${refDef.transformer}" from "${refDef.path}": ${error.message}`);
26
+ throw new ConfigError(`Error calling transformer "${refDef.transformer}" from "${refDef.path}".`, {
27
+ cause: error,
28
+ filePath: refDef.transformer
29
+ });
26
30
  }
27
31
  }
28
32
  return input;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -14,21 +14,36 @@
14
14
  limitations under the License.
15
15
  */ import basicTypes from '@lowdefy/blocks-basic/types';
16
16
  import loaderTypes from '@lowdefy/blocks-loaders/types';
17
+ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
18
+ import findSimilarString from '../utils/findSimilarString.js';
17
19
  function buildTypeClass(context, { counter, definitions, store, typeClass, warnIfMissing = false }) {
18
20
  const counts = counter.getCounts();
21
+ const definedTypes = Object.keys(definitions);
19
22
  Object.keys(counts).forEach((typeName)=>{
20
23
  if (!definitions[typeName]) {
24
+ const configKey = counter.getLocation(typeName);
25
+ let message = `${typeClass} type "${typeName}" was used but is not defined.`;
26
+ const suggestion = findSimilarString({
27
+ input: typeName,
28
+ candidates: definedTypes
29
+ });
30
+ if (suggestion) {
31
+ message += ` Did you mean "${suggestion}"?`;
32
+ }
21
33
  if (warnIfMissing) {
22
- if (typeName === '_id') {
23
- return;
24
- }
25
- context.logger.warn(`${typeClass} type "${typeName}" was used but is not defined.`);
34
+ context.handleWarning(new ConfigWarning(message, {
35
+ configKey,
36
+ checkSlug: 'types'
37
+ }));
26
38
  return;
27
39
  }
28
- throw new Error(`${typeClass} type "${typeName}" was used but is not defined.`);
40
+ throw new ConfigError(message, {
41
+ configKey,
42
+ checkSlug: 'types'
43
+ });
29
44
  }
30
45
  store[typeName] = {
31
- originalTypeName: definitions[typeName].originalTypeName,
46
+ originalTypeName: definitions[typeName].originalTypeName ?? typeName,
32
47
  package: definitions[typeName].package,
33
48
  version: definitions[typeName].version,
34
49
  count: counts[typeName]
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -0,0 +1,35 @@
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
+ 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;
@@ -0,0 +1,36 @@
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
+ 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
+ }
33
+ });
34
+ return typeNames;
35
+ }
36
+ export default collectTypeNames;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -12,11 +12,11 @@
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 jsMapParser from './jsMapParser.js';
15
+ */ import jsMapParser from '../buildJs/jsMapParser.js';
16
16
  function buildJs({ components, context }) {
17
17
  components.pages = components.pages.map((page)=>{
18
18
  const pageRequests = [
19
- ...page.requests
19
+ ...page.requests ?? []
20
20
  ];
21
21
  delete page.requests;
22
22
  const cleanPage = jsMapParser({
@@ -0,0 +1,87 @@
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, shouldSuppressBuildCheck } from '@lowdefy/errors';
17
+ import buildPage from '../buildPages/buildPage.js';
18
+ import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
19
+ import validateLinkReferences from '../buildPages/validateLinkReferences.js';
20
+ import validatePayloadReferences from '../buildPages/validatePayloadReferences.js';
21
+ import validateServerStateReferences from '../buildPages/validateServerStateReferences.js';
22
+ import validateStateReferences from '../buildPages/validateStateReferences.js';
23
+ function buildPages({ components, context }) {
24
+ const pages = type.isArray(components.pages) ? components.pages : [];
25
+ const checkDuplicatePageId = createCheckDuplicateId({
26
+ message: 'Duplicate pageId "{{ id }}".'
27
+ });
28
+ // Initialize linkActionRefs to collect Link action references across all pages
29
+ context.linkActionRefs = [];
30
+ // Track which pages failed to build so we skip them in validation
31
+ const failedPageIndices = new Set();
32
+ // Wrap each page build to collect errors instead of stopping on first error
33
+ pages.forEach((page, index)=>{
34
+ try {
35
+ const result = buildPage({
36
+ page,
37
+ index,
38
+ context,
39
+ checkDuplicatePageId
40
+ });
41
+ // buildPage returns { failed: true } when validation fails
42
+ if (result?.failed) {
43
+ failedPageIndices.add(index);
44
+ }
45
+ } catch (error) {
46
+ // Skip suppressed ConfigErrors (via ~ignoreBuildChecks)
47
+ if (error instanceof ConfigError && shouldSuppressBuildCheck(error, context.keyMap)) {
48
+ return;
49
+ }
50
+ // Collect error object if context.errors exists, otherwise throw (for backward compat with tests)
51
+ if (context?.errors) {
52
+ context.errors.push(error);
53
+ failedPageIndices.add(index);
54
+ } else {
55
+ throw error;
56
+ }
57
+ }
58
+ });
59
+ // Validate that all Link actions reference existing pages
60
+ // Include all pages — a link to a broken page is valid; the page error is already reported
61
+ const pageIds = pages.map((page)=>page.pageId);
62
+ validateLinkReferences({
63
+ linkActionRefs: context.linkActionRefs,
64
+ pageIds,
65
+ context
66
+ });
67
+ // Validate that _state references use defined block IDs
68
+ // and _payload references use defined payload keys
69
+ // Skip pages that failed to build
70
+ pages.forEach((page, index)=>{
71
+ if (failedPageIndices.has(index)) return;
72
+ validateStateReferences({
73
+ page,
74
+ context
75
+ });
76
+ validatePayloadReferences({
77
+ page,
78
+ context
79
+ });
80
+ validateServerStateReferences({
81
+ page,
82
+ context
83
+ });
84
+ });
85
+ return components;
86
+ }
87
+ export default buildPages;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
16
16
  import buildAuth from '../buildAuth/buildAuth.js';
17
17
  import buildPages from './buildPages.js';
18
18
  import createContext from '../../createContext.js';
19
- function buildTestPage({ pageConfig }) {
19
+ function buildTestPage({ pageConfig, connectionIds = [] }) {
20
20
  const context = createContext({
21
21
  customTypesMap: {},
22
22
  directories: {},
@@ -24,10 +24,22 @@ function buildTestPage({ pageConfig }) {
24
24
  debug: ()=>{},
25
25
  log: ()=>{},
26
26
  warn: ()=>{},
27
- error: ()=>{}
27
+ error: ()=>{},
28
+ ui: {
29
+ warn: ()=>{},
30
+ error: ()=>{}
31
+ }
28
32
  },
29
33
  stage: 'test'
30
34
  });
35
+ // Add any connectionIds from test config to allow validation to pass
36
+ connectionIds.forEach((id)=>context.connectionIds.add(id));
37
+ // Also extract connectionIds from requests in the pageConfig
38
+ (pageConfig.requests || []).forEach((request)=>{
39
+ if (request.connectionId) {
40
+ context.connectionIds.add(request.connectionId);
41
+ }
42
+ });
31
43
  const components = {
32
44
  pages: [
33
45
  pageConfig
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -12,9 +12,17 @@
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 { serializer } from '@lowdefy/helpers';
15
+ */ import { serializer, type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/errors';
16
17
  async function writeRequestsOnPage({ page, context }) {
17
- return Promise.all(page.requests.map(async (request)=>{
18
+ const requests = page.requests ?? [];
19
+ if (!type.isArray(requests)) {
20
+ throw new ConfigError('Page requests must be an array.', {
21
+ received: requests,
22
+ configKey: page['~k']
23
+ });
24
+ }
25
+ return Promise.all(requests.map(async (request)=>{
18
26
  await context.writeBuildArtifact(`pages/${page.pageId}/requests/${request.requestId}.json`, serializer.serializeToString(request ?? {}));
19
27
  delete request.properties;
20
28
  delete request.type;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.