@lowdefy/build 0.0.0-experimental-20260113090459 → 0.0.0-experimental-20260114142524
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.
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
import formatConfigWarning from '../../utils/formatConfigWarning.js';
|
|
17
17
|
import traverseConfig from '../../utils/traverseConfig.js';
|
|
18
18
|
function validateStateReferences({ page, context }) {
|
|
19
|
-
// Single traversal collects
|
|
19
|
+
// Single traversal collects blockIds, _state references, and SetState keys
|
|
20
20
|
// More memory-efficient than stringify+regex for massive pages
|
|
21
21
|
const blockIds = new Set();
|
|
22
|
+
const setStateKeys = new Set();
|
|
22
23
|
const stateRefs = new Map(); // topLevelKey -> configKey (first occurrence)
|
|
23
24
|
traverseConfig({
|
|
24
25
|
config: page,
|
|
@@ -27,6 +28,13 @@ function validateStateReferences({ page, context }) {
|
|
|
27
28
|
if (obj.blockId) {
|
|
28
29
|
blockIds.add(obj.blockId);
|
|
29
30
|
}
|
|
31
|
+
// Collect SetState action params to track state keys being initialized
|
|
32
|
+
if (obj.type === 'SetState' && obj.params) {
|
|
33
|
+
Object.keys(obj.params).forEach((key)=>{
|
|
34
|
+
const topLevelKey = key.split(/[.\[]/)[0];
|
|
35
|
+
setStateKeys.add(topLevelKey);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
30
38
|
// Collect _state reference if present
|
|
31
39
|
if (obj._state !== undefined) {
|
|
32
40
|
const topLevelKey = extractOperatorKey({
|
|
@@ -40,7 +48,8 @@ function validateStateReferences({ page, context }) {
|
|
|
40
48
|
});
|
|
41
49
|
// Filter to only undefined references and warn
|
|
42
50
|
stateRefs.forEach((configKey, topLevelKey)=>{
|
|
43
|
-
if
|
|
51
|
+
// Skip if state key is from an input block or SetState action
|
|
52
|
+
if (blockIds.has(topLevelKey) || setStateKeys.has(topLevelKey)) return;
|
|
44
53
|
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
54
|
context.logger.warn(formatConfigWarning({
|
|
46
55
|
message,
|
|
@@ -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
|
|
15
|
+
*/ import path from 'path';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
16
17
|
async function getConfigFile({ context, refDef, referencedFrom }) {
|
|
17
18
|
if (!type.isString(refDef.path)) {
|
|
18
19
|
throw new Error(`Invalid _ref definition ${JSON.stringify({
|
|
@@ -21,8 +22,23 @@ async function getConfigFile({ context, refDef, referencedFrom }) {
|
|
|
21
22
|
}
|
|
22
23
|
const content = await context.readConfigFile(refDef.path);
|
|
23
24
|
if (content === null) {
|
|
25
|
+
// Build helpful error message with resolved path information
|
|
24
26
|
const lineInfo = refDef.lineNumber ? `:${refDef.lineNumber}` : '';
|
|
25
|
-
|
|
27
|
+
const absolutePath = path.resolve(context.directories.config, refDef.path);
|
|
28
|
+
let message = `[Config Error] Referenced file does not exist: "${refDef.path}"\n`;
|
|
29
|
+
message += ` Referenced from: ${referencedFrom}${lineInfo}\n`;
|
|
30
|
+
message += ` Resolved to: ${absolutePath}\n`;
|
|
31
|
+
// Help with common mistakes
|
|
32
|
+
if (refDef.path.startsWith('../')) {
|
|
33
|
+
const suggestedPath = refDef.path.replace(/^(\.\.\/)+/, '');
|
|
34
|
+
message += `\n Tip: Paths in _ref are resolved from your config directory root.`;
|
|
35
|
+
message += `\n Did you mean: "${suggestedPath}"?`;
|
|
36
|
+
} else if (refDef.path.startsWith('./')) {
|
|
37
|
+
const suggestedPath = refDef.path.substring(2);
|
|
38
|
+
message += `\n Tip: Remove the "./" prefix - paths are resolved from config root.`;
|
|
39
|
+
message += `\n Did you mean: "${suggestedPath}"?`;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(message);
|
|
26
42
|
}
|
|
27
43
|
return content;
|
|
28
44
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { serializer, type } from '@lowdefy/helpers';
|
|
16
16
|
import evaluateBuildOperators from './evaluateBuildOperators.js';
|
|
17
|
+
import formatConfigError from '../../utils/formatConfigError.js';
|
|
17
18
|
import getKey from './getKey.js';
|
|
18
19
|
import getRefContent from './getRefContent.js';
|
|
19
20
|
import getRefsFromFile from './getRefsFromFile.js';
|
|
@@ -21,19 +22,28 @@ import populateRefs from './populateRefs.js';
|
|
|
21
22
|
import runTransformer from './runTransformer.js';
|
|
22
23
|
async function recursiveBuild({ context, refDef, count, referencedFrom, refChainSet = new Set(), refChainList = [] }) {
|
|
23
24
|
// Detect circular references by tracking the chain of files being resolved
|
|
25
|
+
// Skip circular reference checking for refs without paths (e.g., resolver refs)
|
|
24
26
|
const currentPath = refDef.path;
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (currentPath) {
|
|
28
|
+
if (refChainSet.has(currentPath)) {
|
|
29
|
+
const chainDisplay = [
|
|
30
|
+
...refChainList,
|
|
31
|
+
currentPath
|
|
32
|
+
].join('\n -> ');
|
|
33
|
+
throw new Error(formatConfigError({
|
|
34
|
+
message: `Circular reference detected.\nFile "${currentPath}" references itself through:\n -> ${chainDisplay}`,
|
|
35
|
+
context
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
refChainSet.add(currentPath);
|
|
39
|
+
refChainList.push(currentPath);
|
|
31
40
|
}
|
|
32
|
-
refChainSet.add(currentPath);
|
|
33
|
-
refChainList.push(currentPath);
|
|
34
41
|
// Keep count as a fallback safety limit
|
|
35
42
|
if (count > 10000) {
|
|
36
|
-
throw new Error(
|
|
43
|
+
throw new Error(formatConfigError({
|
|
44
|
+
message: `Maximum recursion depth of references exceeded (10000 levels). This likely indicates a circular reference.`,
|
|
45
|
+
context
|
|
46
|
+
}));
|
|
37
47
|
}
|
|
38
48
|
let fileContent = await getRefContent({
|
|
39
49
|
context,
|
|
@@ -97,8 +107,11 @@ async function recursiveBuild({ context, refDef, count, referencedFrom, refChain
|
|
|
97
107
|
});
|
|
98
108
|
}
|
|
99
109
|
// Backtrack: remove current file from chain so sibling refs can use it
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
// Only remove if it was added (i.e., if currentPath exists)
|
|
111
|
+
if (currentPath) {
|
|
112
|
+
refChainSet.delete(currentPath);
|
|
113
|
+
refChainList.pop();
|
|
114
|
+
}
|
|
102
115
|
return populateRefs({
|
|
103
116
|
toPopulate: fileContentBuiltRefs,
|
|
104
117
|
parsedFiles,
|